19
votes

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

3
None of the answers actually answer the question as to how animations work.. In iOS animations work on something called a presentation layer. It animates using a snapshot with interpolation from one place to another. Example: When you call UIView.animate, it takes a snapshot of the view to be animated. It takes note of all property values.. Then it calls your block which changes properties values.. It notes the difference in the values and interpolates over time, the snapshot from original values to new values (ones inside animation block).. When it is done, it will update your actual view.Brandon
updateViewConstraints is called when constraints are implicitly updated, not explicitly changed. You didn't invalidate any constraints in an animation. Animate works on the layer's position and bounds. When you call layoutIfNeeded, it will layout the view on the next render cycle/pass (queue). Then you animate.. When you are done, it will layout your actual view and destroy the snapshot.. You will always notice your animation block only ever called once. LayoutSubviews will be called once at the END of your animation (because snapshot is animated, not view itself).Brandon
@Brandon 1. "You didn't invalidate any constraints in an animation" Then What did I do here: self.centerXConstraint.isActive = !self.centerXConstraint.isActive Did I not invalidate a constraint?! 2. Or is it that this is an explicit change hence updateViewConstraints isn't called? ok it's not called, then what sort of callback does it trigger to complete the change/animation...mfaani
You didn't invalidate anything. In your animation block, it only checks animatable properties. .isActive isn't animatable. Its state isn't snapshotted. Therefore, that property isn't actually animated, it's changed immediately. You then call layoutIfNeeded. The animation block will interpolate from its current position to the final position which was set when you modified isActive. All it does is animate the snapshot. Then it calls layoutSubviews. Try rotating your device and you'll see updateConstraints called.Brandon
You didn't invalidate any constraints by saying isActive = false. That's not an invalid constraint. It's an in-active constraint and it still exists. An invalid constraint is a constraint that is left dangling such as when a view moves to a different super-view or device rotated, etc.. When a constraint is invalid, the system needs to recalculate the entire layout. It doesn't need to calculate your layout again, it just needs to move its position.. Also, updateConstraints is called on the view that is modified. It seems you are trying harder NOT to understand it than it is to understand.Brandon

3 Answers

56
votes

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

Additional resources:

2
votes

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.

2
votes

I will try to explain it simply:

The first thing to remember is that updating constraints does not 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.

Edit to hopefully more directly answer the two questions:

1) 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?

First you have misunderstood in your readings you don't need to use setNeedsUpdateConstraints at all. Secondly they are enough (assuming they are in an animation block) because the setNeedsLayout() flags that self.view needs to have it's layout (and therefore its sub views layouts) re-calculated and the 'layoutIfNeeded()' forces the layout to take place immediately and therefore if inside an animation block to be done at each update of the animation.

2) 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?

Best to show with your original example of this:

_addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}

The first line has updated the constraint by changing its constant (no need to use setNeedsUpdateConstraints) but the layout of the view (i.e. it's actual frame position and size) has not yet changed. When you call self.view.layoutIfNeeded() within the animation block that updates the layout of self.view which based on the current time frame of the animation. It is at this point that the frame position/size of views is calculated and adjusted.

I hope that makes it clearer but in reality your questions have been answered in detail in the body of the question maybe it was too detailed of an explanation though.

Now to help clarity EVERY view on the screen has a frame controlling both its size and position. This frame is either set manually via the property or is calculated using the constraints you have setup. Regardless of the method it's the frame that determines the position and size of the view not the constraints. The constraints are just used to calculate the frame of a view.

To try to make it even clearer I will now add two examples that achieve the same thing but using the two different methods. For both there is a testView which has constraints putting it in the centre of the main view controller view (these won't be changing and can effectively be ignored for the example). There is also a widthConstraint and a heightConstraint for that testView which will be used to control the height and width of the view. There is an expanded bool property which determines whether the testView is expanded or not and a testButton which is used to toggle between expanded and collapsed states.

The first way of doing it is this:

class ViewController: UIViewController {
    @IBOutlet var testView: UIView!
    @IBOutlet var testButton: UIButton!
    @IBOutlet var widthConstraint: NSLayoutConstraint!
    @IBOutlet var heightConstraint: NSLayoutConstraint!

    var expanded = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func testButtonAction(_ sender: Any) {
        self.expanded = !self.expanded

        if self.expanded {
            self.widthConstraint.constant = 200
            self.heightConstraint.constant = 200
        } else {
            self.widthConstraint.constant = 100
            self.heightConstraint.constant = 100
        }
        self.view.layoutIfNeeded() // You only need to do this if you want the layout of the to be updated immediately.  If you leave it out the system will decide the best time to update the layout of the test view.
    }

}

and here when the button is tapped the expanded bool property is toggled and then the constraints are immediately updated by changing their constants. layoutIfNeeded is then called to re-calculate the layout of the testView immediately (thus updating the display) although this could be left out leaving the system to re-calculate the layout based on the new constraint values when it needs to.

Now here is another way of doing the same thing:

class ViewController: UIViewController {
    @IBOutlet var testView: UIView!
    @IBOutlet var testButton: UIButton!
    @IBOutlet var widthConstraint: NSLayoutConstraint!
    @IBOutlet var heightConstraint: NSLayoutConstraint!

    var expanded = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func testButtonAction(_ sender: Any) {
        self.expanded = !self.expanded
        self.view.setNeedsUpdateConstraints()
    }

    override func updateViewConstraints() {
        super.updateViewConstraints()
        if self.expanded {
            self.widthConstraint.constant = 200
            self.heightConstraint.constant = 200
        } else {
            self.widthConstraint.constant = 100
            self.heightConstraint.constant = 100
        }
    }
}

and here when the button is tapped the 'expanded' bool property is toggled and we use updateConstraintsIfNeeded to flag to the system that the constraints will need to be updated before the layout can be re-calculated (whenever it may be the system determines that is needed). When the system needs to know those constraints to re-calculate the layout of the views (something it decides) it automatically calls updateViewConstraints and the constraints are changed at this time to their new values.

So if you try it these both do fundamentally the same thing as they stand but there are different use cases for them.

Using method 1 allows animation because (as has been noted) you can wrap the layoutIfNeeded in an animation block like this:

    UIView.animate(withDuration: 5) {
        self.view.layoutIfNeeded()
    }

which causes the system to animate between the initial layout and the new layout based on the constraint changes since the last time the layout was calculated.

Using method 2 allows you to postpone the need to change constraints until they are absolutely needed and you would want to do this when your constraints are really complex (lots of them) or there could be lots of actions that happen that could require the constraints to be changed before the next layout re-calculation is needed (to avoid continually changing constraints when not needed). Doing this though you lack the ability to animate the changes but that's probably not an issue as the complexity of the constraints would make everything slow to a crawl anyway.

I hope this helps more.