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:
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:
I appreciate any help with this frustrating issue!
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.)