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.
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. – BrandonupdateViewConstraints
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 calllayoutIfNeeded
, 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). – Brandonself.centerXConstraint.isActive = !self.centerXConstraint.isActive
Did I not invalidate a constraint?! 2. Or is it that this is an explicit change henceupdateViewConstraints
isn't called? ok it's not called, then what sort of callback does it trigger to complete the change/animation... – mfaani.isActive
isn't animatable. Its state isn't snapshotted. Therefore, that property isn't actually animated, it's changed immediately. You then calllayoutIfNeeded
. The animation block will interpolate from its current position to the final position which was set when you modifiedisActive
. All it does is animate the snapshot. Then it callslayoutSubviews
. Try rotating your device and you'll seeupdateConstraints
called. – BrandonisActive = 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