From this answer:
This is what the accepted answer suggests to animate your view changes:
_addBannerDistanceFromBottomConstraint.constant = 0
UIView.animate(withDuration: 5) {
self.view.layoutIfNeeded()
}
Why do we call layoutIfNeeded
when we aren't changing the frames. We are changing the constraints, so (according to this other answer) shouldn't we instead be calling setNeedsUpdateConstraints
?
Similarly this highly viewed answer says:
If something changes later on that invalidates one of your constraints, you should remove the constraint immediately and call setNeedsUpdateConstraints
I actually did try using them both.
Using setNeedsLayout
my view animates correctly to the left
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func animate(_ sender: UIButton) {
UIView.animate(withDuration: 1.8, animations: {
self.centerXConstraint.isActive = !self.centerXConstraint.isActive
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
})
}
@IBOutlet weak var centerYConstraint: NSLayoutConstraint!
@IBOutlet var centerXConstraint: NSLayoutConstraint!
}
However using setNeedsUpdateConstraints
doesn't animate, It just moves the view rapidly to the left.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func animate(_ sender: UIButton) {
UIView.animate(withDuration: 1.8, animations: {
self.centerXConstraint.isActive = !self.centerXConstraint.isActive
self.view.setNeedsUpdateConstraints()
self.view.updateConstraintsIfNeeded()
})
}
@IBOutlet weak var centerYConstraint: NSLayoutConstraint!
@IBOutlet var centerXConstraint: NSLayoutConstraint!
}
If I don't want animation then using either of view.setNeedsLayout
or view.setNeedsUpdateConstraints
move it to the left. However:
view.setNeedsLayout
, after my button is tapped, my viewDidLayoutSubviews
breakpoint is reached. But the updateViewConstraints
breakpoint is never reached. This leaves me baffled as to how the constraints are getting updated...view.setNeedsUpdateConstraints
, after the button is tapped my updateViewConstraints
breakpoint is reached and then the viewDidLayoutSubviews
breakpoint is reached. This does make sense, the constraints are updated, then the layoutSubviews is called.Based on my readings: if you change constraints then for it to become effective you MUST call setNeedsUpdateConstraints
, but based on my observations that's wrong. Having the following code was enough to animate:
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
WHY?
Then I thought maybe somehow under the hoods it's updating the constraints through other means. So I placed a breakpoint at override func updateViewConstraints
and override func viewDidLayoutSubviews
but only the viewDidLayoutSubviews
reached its breakpoint.
So how is the Auto Layout engine managing this?
This is a common misunderstanding among iOS developers.
Here's one of my "golden rules" for Auto Layout:
You never need to call any of these methods:
setNeedsUpdateConstraints()
updateConstraintsIfNeeded()
updateConstraints()
updateViewConstraints()
except for the very rare case that you have a tremendously complex layout which slows down your app (or you deliberately choose to implement layout changes in an atypical way).
Normally, when you want to change your layout, you would activate / deactivate or change layout constraints directly after a button tap or whichever event triggered the change, e.g. in a button's action method:
@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
toggleLayout()
}
func toggleLayout() {
isCenteredLayout = !isCenteredLayout
if isCenteredLayout {
centerXConstraint.isActive = true
} else {
centerXConstraint.isActive = false
}
}
As Apple puts it in their Auto Layout Guide:
It is almost always cleaner and easier to update a constraint immediately after the affecting change has occurred. Deferring these changes to a later method makes the code more complex and harder to understand.
You can of course also wrap this constraint change in an animation: You first perform the constraint change and then animate the changes by calling layoutIfNeeded()
in the animation closure:
@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
// 1. Perform constraint changes:
toggleLayout()
// 2. Animate the changes:
UIView.animate(withDuration: 1.8, animations: {
view.layoutIfNeeded()
}
}
Whenever you change a constraint, the system automatically schedules a deferred layout pass, which means that the system will recompute the layout in the near future. No need to call setNeedsUpdateConstraints()
because you just did update (change) the constraint yourself! What needs to be updated is the layout i.e. the frames of all your views, not any other constraint.
As previously stated, the iOS layout system usually doesn't react immediately to constraint changes but only schedules a deferred layout pass. That's for performance reasons. Think of it like this:
When you go shopping groceries, you put an item in your cart but you don't pay it immediately. Instead, you put other items in your cart until you feel like you got everything you need. Only then you proceed to the cashier and pay all your groceries at once. It's way more efficient.
Due to this deferred layout pass there is a special mechanism needed to handle layout changes. I call it The Principle of Invalidation. It's a 2-step mechanism:
In terms of the layout engine this corresponds to:
setNeedsLayout()
layoutIfNeeded()
and
setNeedsUpdateConstraints()
updateConstraintsIfNeeded()
The first pair of methods will result in an immediate (not deferred) layout pass: First you invalidate the layout and then you recompute the layout immediately if it's invalid (which it is, of course).
Usually you don't bother if the layout pass will happen now or a couple of milliseconds later so you normally only call setNeedsLayout()
to invalidate the layout and then wait for the deferred layout pass. This gives you the opportunity to perform other changes to your constraints and then update the layout slightly later but all at once (→ shopping cart).
You only need to call layoutIfNeeded()
when you need the layout to be recomputed right now. That might be the case when you need to perform some other calculations based on the resulting frames of your new layout.
The second pair of methods will result in an immediate call of updateConstraints()
(on a view or updateViewConstraints()
on a view controller). But that's something you normally shouldn't do.
Only when your layout is really slow and your UI feels laggy due to your layout changes you can choose a different approach than the one stated above: Rather than updating a constraint directly in response to a button tap you just make a "note" of what you want to change and another "note" that your constraints need to be updated.
@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
// 1. Make a note how you want your layout to change:
isCenteredLayout = !isCenteredLayout
// 2. Make a note that your constraints need to be updated (invalidate constraints):
setNeedsUpdateConstraints()
}
This schedules a deferred layout pass and ensures that updateConstraints()
/ updateViewConstraints()
will be called during the layout pass. So you may now even perform other changes and call setNeedsUpdateConstraints()
a thousand times – your constraints will still only be updated once during the next layout pass.
Now you override updateConstraints()
/ updateViewConstraints()
and perform the necessary constraint changes based on your current layout state (i.e. what you have "noted" above in "1."):
override func updateConstraints() {
if isCenteredLayout {
centerXConstraint.isActive = true
} else {
centerXConstraint.isActive = false
}
super.updateConstraints()
}
Again, this is only your last resort if the layout is really slow and you're dealing will hundreds or thousands of constraints. I have never needed to use updateConstraints()
in any of my projects, yet.
I hope this make things a little clearer.
setNeedsUpdateConstraints
will update the constraints that will be changed based on a change you have made. For example if your view has a neighboring view with which there a constraint of horizontal distance, and that neighbor view got removed, the constraint is invalid now. In this case you should remove that constraint and call setNeedsUpdateConstraints
. It basically makes sure that all your constraints are valid. This will not redraw the view. You can read more about it here.
setNeedsLayout
on the other hand marks the view for redrawing and putting it inside animation block makes the drawing animated.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With