简体   繁体   中英

UIView constraints won't animate if layoutIfNeeded() is called

I'm trying to animate a view that is constrained ( iconVerticalConstraint ) to be at center Y ( +100 ) of a superview, specifically reaching its right ( 0 ) position after an animation. Here's my code:

self.view.layoutIfNeeded()

UIView.animateKeyframes(withDuration: 2, delay: 0, animations: {

    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations:{
        self.iconVerticalConstraint.constant -= 20
    })

    UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations:{
        self.iconVerticalConstraint.constant -= 80
    })
})

I tried following this question's answer but that didn't help, because in my case my view contains more subviews, intrinsically linked, and putting layoutIfNeeded() in my animation block would animate the whole view, and not just that constraint. Otherwise this approach does no animation at all.

changing constraints such as

self.iconVerticalConstraint.constant -= 20

should be OUTSIDE the animation block and inside you just call

self.view.layoutIfNeeded() 

I think because you are trying to do 2, and calling

self.view.layoutIfNeeded() 

They all happen once, consider changing to:

    self.iconVerticalConstraint.constant -= 20

    UIView.animate(withDuration: 0.25, animations: {
        self.view.layoutIfNeeded()
    }) { (finished) in
     
        self.iconVerticalConstraint.constant -= 80

        UIView.animate(withDuration: 0.75, animations: {
            self.view.layoutIfNeeded()
        }, completion: nil)
    }

Edit: Explanation

When you change a constraint, the view will not update until the next layout cycle, usually when you exit your current scope. So when you want to animate a constraint, you change the constant BEFORE the animation block, then inside the animation block you call layoutIfNeeded() This tells it to animate the now forced layout cycle.

Now in your example, you where calling layoutIfNeeded() which would do nothing at that point, then changing 2 constants, without telling the layout to animate, so when the scope exited, the UI would layout without animation.

In your case you want to chain the changing of 2 constants, but you dont have a way of changing the second constant (80) after the first animation completes, and if you set both, both constants will be adjusted on the FIRST layoutIfNeeded() hence, the chaining of animations, which will still work with your other animations that you omitted, just add then in the first animation block if they are in the first keyframe and the second if they were in the second.

Final Edit:

Also you can change your layoutIfNeeded() currently applied to self.view to actually on the views it affects, in this case iconVerticalConstraint if it was a view that is attached to another view say a top attached to a view bottom, if you wanna animate, you must call layoutIfNeeded() on each view it affects

After hours of time spent to understand the basic mechanism, I can provide my own answer, for all of you who will find themselves in my situation.

layoutIfNeeded() tells to the view to adjust position of all of its subviews . animate or animateKeyframes or every other alternative you're using, instead, tells to the view to perform any of the instructions inside the animation block in the way you specify in the animate function (by default, with a options: .easeInOut )

Back to the question: the problem was in the fact that animate and layoutIfNeeded are called asynchronously so that, in fact, every instruction inside the animation block was executed just before the layoutIfNeeded instruction, making it useless. The solution isn't putting the layoutIfNeeded instruction inside the animation block, as suggested by SeanLintern88 because in this way we would tell the view to adjust the position of its subviews according to the animation options . That's the key point.

The answer is somewhat simple. Move the animation part on the subview itself , specifically in a point when you know that the superview has already been "autolayouted" (eg viewDidAppear ), so that the constraint is already defined and executed, and you can now change it. In that point, code should look like this:

UIView.animateKeyframes(withDuration: 2, delay: 0, animations: {

    self.view.layoutIfNeeded()

    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations:{
        self.iconVerticalConstraint.constant -= 20
    })

    UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations:{
        self.iconVerticalConstraint.constant -= 80
    })
})

Because you want to animate the autolayout.

Another option (for instance, if you haven't/don't want to create a class for your subview) is to change bounds/center of the subview itself in the viewDidAppear function of the parent view. This is because, if you are in the parent view, you know that at that point auto layout has already been registered but not executed, in the more precise sense that bounds and center have been already changed according to the auto layout.

If you then change them, you'll obtain your desired animation (but remember : auto layout is still on, so you must change the constraints in a way that reflects the final position of your animation; if you wish, you can do that in the completion part of animate function).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM