Pierce Pierce - 1 month ago 34
Swift Question

UIView animateWithDuration Animating Auto-Layout Views Outside of Animation Block

I'm noticing at various times throughout my app that when I call

UIView.animateWithDuration
, if I have any other kind of auto-layout requests, or even something as simple as changing the constant of an
NSLayoutConstraint
, those changes will also be animated even when they are NOT inside the animation block. For example: I have a
UIViewController
that right after
viewDidAppear
is called, will add an image which was chosen by the user to a
UIScrollView
so that it can be resized and edited. I'm working on adding a little demonstration view with some GIF's to help the user when they first use the app. Once the view shows, it should add the image (I've set it to fit to their screen at first) to the scrollView, and then animate the
demonstrationView
alpha from zero to 1.0. The problem I'm experiencing, is even though my function to add and resize the image
assignImageChosen
is called before
animateWithDuration
, and is outside the animation block, the changes in size constraints are also animated.

override func viewDidAppear(animated: Bool) {

if let image = startImage {
self.assignImageChosen(image)
currentlyEditing = true
}

if preLoadGame {

UIView.animateWithDuration(1.0, delay: 0.5, options: UIViewAnimationOptions.CurveEaseInOut, animations: {


self.demonstrationView.alpha = 1.0
self.view.layoutIfNeeded()

}, completion: nil)

}

}


Here is the function
assignImageChosen
:

func assignImageChosen(image: UIImage) {

var ratio:CGFloat!
let screen:CGFloat = max(self.scrollView.frame.height, self.scrollView.frame.width)
var maxImage:CGFloat!
if UIDevice.currentDevice().orientation.isPortrait {
if image.size.height >= image.size.width {
maxImage = max(image.size.height, image.size.width)
}
else {
maxImage = min(image.size.height, image.size.width)
}
}
else {
if image.size.width >= image.size.height {
maxImage = max(image.size.height, image.size.width)
}
else {
maxImage = min(image.size.height, image.size.width)
}
}

if maxImage > screen {
ratio = screen/maxImage
}
else {
ratio = 1.0
}

self.imageView.image = image
self.imageHeight.constant = image.size.height * ratio
self.scrollView.contentSize.height = image.size.height * ratio
self.imageWidth.constant = image.size.width * ratio
self.scrollView.contentSize.width = image.size.width * ratio

}


The variables
imageHeight
and
imageWidth
are both
NSLayoutConstraint
's, and
imageView
is the
UIImageView
to which they are set. So once I change the image, I change the constraints so that the imageView inside the
UIScrollView
is fit to the screen. The problem is that when I do it like this, even though the changes are called outside the animation block, those changes are still animated.

Here's a GIF example of what I'm trying (poorly) to communicate through words:

issue

I have found a way to circumvent this issue, but it feels like a complete hack. I manually code the system to delay the execution of the animation by using the
dispatch_after
command like so:

override func viewDidAppear(animated: Bool) {

if let image = startImage {
self.assignImageChosen(image)
currentlyEditing = true
}

if preLoadGame {

let delay = 0.5 * Double(NSEC_PER_SEC)
let popTime = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(popTime, dispatch_get_main_queue(), {
UIView.animateWithDuration(1.0, delay: 0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {


self.demonstrationView.alpha = 1.0
self.view.layoutIfNeeded()

}, completion: nil)
})

}

}


So instead of using the
delay
variable inside the
animateWithDuration
function, I instead delay the execution of the function. It works, but I would think there must be a better way to do this! Why are those layout changes being animated when they aren't inside the animation block? How can I get around this in a proper, efficient manner?

Here is how it looks with my hack, and how I would imagine it should look without it:

with hack

I appreciate any help with this frustrating issue!

Answer

This is happening because calling layoutIfNeeded in the animation block is causing all pending layout changes to have their new values calculated for use by the animation. In fact, this is the way to animate changes with constraints, to set them outside the animation block and then call layoutIfNeeded in the block; the new values are calculated and used for the animation.

Fortunately the solution seems to be as simple as could be hoped, to call layoutIfNeeded before doing the animation, before calling it again in the block. According to this answer and others this is in fact Apple's official recommendation, though the link they provide to apple.com is broken.

(By the way, about your hack, there are some cases where you need to stagger changes to after UIKit has completed its layout pass, for example to do something on a UITableView after reloadData has completed. The "correct hack" is to do a UIViewAnimation, then in its completion block do the second animation, keeping the sequencing it in UIKit land.)