问题
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, myviewDidLayoutSubviews
breakpoint is reached. But theupdateViewConstraints
breakpoint is never reached. This leaves me baffled as to how the constraints are getting updated... - with
view.setNeedsUpdateConstraints
, after the button is tapped myupdateViewConstraints
breakpoint is reached and then theviewDidLayoutSubviews
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?
回答1:
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:
- You mark something as invalid.
- If something is invalid, you perform some action to make it valid again.
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.
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:
- Auto Layout – From Leading to Trailing: my talk from UIKonf 2017, topics:
- "The Layout Pass" and
- "Updating Constraints"
- The Auto Layout Comprehendium™: scroll down to section "Updating Constraints", maintained by me
- The Auto Layout Guide by Apple: sections
- "Changing Constraints"
- "The Deferred Layout Pass"
回答2:
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.
回答3:
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.
来源:https://stackoverflow.com/questions/47823639/why-calling-setneedsupdateconstraints-isnt-needed-for-constraint-changes-or-ani