Tuesday, December 26, 2017

Why calling setNeedsUpdateConstraints isn't needed for constraint changes or animations?

Leave a Comment

Readings:

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 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

Observations:

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:

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

Questions:

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?

4 Answers

Answers 1

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.

Answers 2

A Playground

AutoLayout Playground with a button and a view. Any time you're suffering start with a Playground!

This is wrong

For starters... what is the view's horizontal position once the X constraint is disabled? You've left it ambiguous. Check out my Playground and how I animate from the left-side to the centre such that you can just keep tapping the button and play bounce the view. Reading the coverage of updating constraints and layouts it only makes sense to me to perform the animation in the layout phase.

@IBAction func animate(_ sender: UIButton) {      UIView.animate(withDuration: 1.8, animations: {         self.centerXConstraint.isActive = !self.centerXConstraint.isActive         self.view.setNeedsLayout()         self.view.layoutIfNeeded()     }) } 

This is also wrong

@IBAction func animate(_ sender: UIButton) {      UIView.animate(withDuration: 1.8, animations: {     self.centerXConstraint.isActive = !self.centerXConstraint.isActive         self.view.setNeedsUpdateConstraints()         self.view.updateConstraintsIfNeeded()         }) }     

In essence you're saying, set up this animation by suggesting you do it and then you force it. So there is no need for both.

`setNeeds...()` - is a suggestion  `...IfNeeded()` - is an imperative (AKA Now!) 

Here you seem my formulation. In the layout pass establish the Starting layout before the animation block and the ending layout in the block. Then iOS will correctly animate between the two points. Note - I don't leave any ambiguity along the horizontal axis.

@objc func animate(sender: AnyObject) {     guard let innerView = self.innerView,         let innerViewLeadingConstraint = self.innerViewLeadingConstraint,         let innerViewCenterXConstraint = self.innerViewCenterXConstraint else {         return     }      self.view.layoutIfNeeded()     UIView.animate(withDuration: 1.8, animations: {         if self.view.constraints.contains(innerViewCenterXConstraint) {             self.view.removeConstraint(innerViewCenterXConstraint)             self.view.addConstraint(innerViewLeadingConstraint)         } else {             self.view.removeConstraint(innerViewLeadingConstraint)             self.view.addConstraint(innerViewCenterXConstraint)         }         self.view.layoutIfNeeded()     }) } 

As described at ObjC.io

The first step – updating constraints – can be considered a “measurement pass.” It happens bottom-up (from subview to super view) and prepares the information needed for the layout pass to actually set the views’ frame. You can trigger this pass by calling setNeedsUpdateConstraints. Any changes you make to the system of constraints itself will automatically trigger this. However, it is useful to notify Auto Layout about changes in custom views that could affect the layout. Speaking of custom views, you can override updateConstraints to add the local constraints needed for your view in this phase.

The second step – layout – happens top-down (from super view to subview). This layout pass actually applies the solution of the constraint system to the views by setting their frames (on OS X) or their center and bounds (on iOS). You can trigger this pass by calling setNeedsLayout, which does not actually go ahead and apply the layout immediately, but takes note of your request for later. This way you don’t have to worry about calling it too often, since all the layout requests will be coalesced into one layout pass. To force the system to update the layout of a view tree immediately, you can call layoutIfNeeded/layoutSubtreeIfNeeded (on iOS and OS X respectively). This can be helpful if your next steps rely on the views’ frame being up to date. In your custom views you can override layoutSubviews/layout to gain full control over the layout pass. We will show use cases for this later on.

Autolayout Guide

This further clarifies the rules of AutoLayout when changing size.

Instead of immediately updating the affected views’ frames, Auto Layout schedules a layout pass for the near future. This deferred pass updates the layout’s constraints and then calculates the frames for all the views in the view hierarchy.

You can schedule your own deferred layout pass by calling the setNeedsLayout method or the setNeedsUpdateConstraints method.

The deferred layout pass actually involves two passes through the view hierarchy:

  • The update pass updates the constraints, as necessary
  • The layout pass repositions the view’s frames, as necessary

Mysteries of Auto-Layout

There are a pair of 2015 WWDC talks you should watch.

Answers 3

This is a common misunderstanding among iOS developers.

Here's one of my "golden rules" for Auto Layout:

Don't bother about "updating constraints".

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

The Preferred Way to Change Your Layout

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.

The Principle of Invalidation

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 Princpile of Invalidation. It's a 2-step mechanism:

  1. You mark something as invalid.
  2. If something is invalid, you perform some action to make it valid again.

In terms of the layout engine this corresponds to:

  1. setNeedsLayout()
  2. layoutIfNeeded()

and

  1. setNeedsUpdateConstraints()
  2. 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.

Changing Your Layout in a Batch

Only when your layout is really slow and your UI feels laggy due to your layout changes you can choose a different approach that 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.

Additional resources:

Answers 4

I will try to explain it simply:

The first thing to remember is that updating constraints does cause the layout of views to be updated immediately. This is for performance reasons as laying everything out can take time so it 'makes note' of changes that need to take place then does a single layout pass.

Taking that one step further you can then not even update constraints when something affecting them changes but just flag that the constraints need to be updated. Even updating the constraints themselves (without laying out the views) can take time and the same ones could change both ways (i.e. active and inactive).

Now considering all that what setNeedsUpdateConstraints() does is to flag that the constraints for a view need to be re-calculated BEFORE the next layout pass because something about them has changed it doesn't make any constraint changes of affect the current layout at all. Then you should implement your own version of the updateConstraints() method to actually make the required changes to the constraints based on the current app state, etc.

So when the system decides the next layout pass should occur anything that has had setNeedsUpdateConstraints() called on it (or the system decides needs updating) will get its implementation of updateConstraints() called to make those changes. This will happen automatically before the laying out is done.

Now the setNeedsLayout() and layoutIfNeeded() are similar but for control of the actual layout processing itself.

When something that affects the layout of a view changes you can call setNeedsLayout() so that that view is 'flagged' to have it's layout re-calculated during the next layout pass. So if you change constraints directly (instead of perhaps using setNeedsUpdateConstraints() and updateConstraints()) you can then call setNeedsLayout() to indicate that the views layout has changed and will need to be re-calculated during the next layout pass.

What layoutIfNeeded() does is to force the layout pass to happen then and there rather than waiting for when the system determines it should next happen. It's that the forces the re-calculation of the layouts of views based on the current sate of everything. Note also that when you do this fist anything that has been flagged with setNeedsUpdateConstraints() will first call it's updateConstraints() implementation.

So no layout changes are made until the system decides to do a layout pass or your app calls layoutIfNeeded().

In practice you rarely need to use setNeedsUpdateConstraints() and implement your own version of updateConstraints() unless something is really complex and you can get by with updating view constraints directly and using setNeedsLayout() and layoutIfNeeded().

So in summary setNeedsUpdateConstraints doesn't need to be called to make constraint changes take affect and in fact if you change constraints they will automatically take affect when the system decides it's time for a layout pass.

When animating you want slightly more control over what is happening because you don't want an immediate change of the layout but to see it change over time. So for simplicity let's say you have an animation that takes a second (a view moves from the left of the screen to the right) you update the constraint to make the view move from left to right but if that was all you did it would just jump from one place to another when the system decided it was time for a layout pass. So instead you do something like the following (assuming testView is a sub view of self.view):

testView.leftPositionConstraint.isActive = false // always de-activate testView.rightPositionConstraint.isActive = true // before activation UIView.animate(withDuration: 1) {     self.view.layoutIfNeeded() } 

Let's break that down:

First this testView.leftPositionConstraint.isActive = false turns off the constraint keeping the view in the left hand position but the layout of the view is not yet adjusted.

Second this testView.rightPositionConstraint.isActive = true turns on the constraint keeping the view in the right hand position but again the layout of the view is not yet adjusted.

Then you schedule the animation and say that during each 'time slice' of that animation call self.view.layoutIfNeeded(). So what that will do is force a layout pass for self.view every time the animation updates causing the testView layout to be re-calculated based on it's position through the animation i.e. after 50% of the animation the layout will be 50% between the stating (current) layout and the required new layout.

Thus doing that the animation takes affect.

So in overall summary:

setNeedsConstraint() - called to inform the system that the constraints of a view will need to be updated because something affecting them has changed. The constraints are not actually updated until the system decides a layout pass is needed or the user forces one.

updateConstraints() - this should be implemented for views to update the constraints based on the apps state.

setNeedsLayout() - this informs the system that something affecting the layout of a view (constraints probably) have changed and the layout will need to be re-calculated during the next layout pass. Nothing happens to the layout at that time.

layoutIfNeeded() - performs a layout pass for the view now rather than waiting for the next system scheduled one. At this point the view and it's sub views layouts will actually be re-calculated.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment