iosswiftuiviewautolayoutanimatewithduration

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!


Solution

  • 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.)