Reputation: 333
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.
Upvotes: 1
Views: 3938
Reputation: 3141
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
Upvotes: 5
Reputation: 333
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" (e.g. 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).
Upvotes: 2