How is layoutIfNeeded used?
layoutIfNeeded
forces the receiver to layout its subviews immediately if required.
Suppose you have overridden layoutSubviews
, and UIKit feels that your view requires layout for whatever reason (e.g. you called setNeedsLayout
when handling some user action). Then, your custom layoutSubviews
method will be called immediately instead of when it would normally be called in the regular UIKit run loop event sequence (after event handling, but before drawRect:
).
An example of why you might need to call layoutIfNeeded
within a single run loop:
- You resize a custom view containing a table view with a custom layout.
setNeedsLayout
is set so thatlayoutSubviews
will be called later. - A controller object asks the table view to scroll to some particular cell when handling a user event.
- Your custom view performs some custom sizing of the table view in
layoutSubviews
that changes the table view size.
The problem is when the controller asked the table view to scroll (step 2), the table view had bounds that were stale. The updated bounds would only be set on the table view later (step 3). What the controller wanted the table view to scroll to may not actually be visible after layoutSubviews
is done. A solution then would be for the controller to call layoutIfNeeded
in situations where it knows this might occur.
What does `self.view.layoutIfNeeded()` do when changing constraints
Essentially calling the self.view.layoutIfNeeded()
will force the layout of self.view
and its subviews if some change set setNeedsLayout
for self.view
.
setNeedsLayout
is used to set a flag that whenever layoutIfNeeded()
is called, layoutSubviews
will then be called. It is what determines the "if needed" aspect of the call.
If you make a change to UIView which causes it to setNeedsLayout
but layoutIfNeeded()
is not called (therefore layoutSubviews is not called) it will not update and therefore could cause issues with your animation. If you call it before hand it will ensure that if there was a change that needed your layout to be updated then it will apply it to your view and all of its subviews before animating.
And of course, when animating you are making changes which need to be updated.
What is the relationship between UIView's setNeedsLayout, layoutIfNeeded and layoutSubviews?
I'm still trying to figure this out myself, so take this with some skepticism and forgive me if it contains errors.
setNeedsLayout
is an easy one: it just sets a flag somewhere in the UIView that marks it as needing layout. That will force layoutSubviews
to be called on the view before the next redraw happens. Note that in many cases you don't need to call this explicitly, because of the autoresizesSubviews
property. If that's set (which it is by default) then any change to a view's frame will cause the view to lay out its subviews.
layoutSubviews
is the method in which you do all the interesting stuff. It's the equivalent of drawRect
for layout, if you will. A trivial example might be:
-(void)layoutSubviews {
// Child's frame is always equal to our bounds inset by 8px
self.subview1.frame = CGRectInset(self.bounds, 8.0, 8.0);
// It seems likely that this is incorrect:
// [self.subview1 layoutSubviews];
// ... and this is correct:
[self.subview1 setNeedsLayout];
// but I don't claim to know definitively.
}
AFAIK layoutIfNeeded
isn't generally meant to be overridden in your subclass. It's a method that you're meant to call when you want a view to be laid out right now. Apple's implementation might look something like this:
-(void)layoutIfNeeded {
if (self._needsLayout) {
UIView *sv = self.superview;
if (sv._needsLayout) {
[sv layoutIfNeeded];
} else {
[self layoutSubviews];
}
}
}
You would call layoutIfNeeded
on a view to force it (and its superviews as necessary) to be laid out immediately.
Is best to call .layoutIfNeeded() before or after constraints are changed
You need to call it after
// after the constraints are changed
self.view.layoutIfNeeded()
Why layoutIfNeeded() allows to perform animation when updating constraints?
As I understand it, changing a constraint doesn't update the layout of your views right away. It queues the changes, and they only take place the next time the system updates layout.
By putting layoutIfNeeded inside your animation block it forces the view changes to be applied during the animation block. Otherwise they take place later, after the animation has been set up.
How to, simply, wait for any layout in iOS?
The window server has final control of what appears on screen. iOS only sends updates to the window server when the current CATransaction
is committed. To make this happen when it is needed, iOS registers a CFRunLoopObserver
for the .beforeWaiting
activity on the main thread's run loop. After handling an event (presumably by calling into your code), the run loop calls the observer before it waits for the next event to arrive. The observer commits the current transaction, if there is one. Committing the transaction includes running the layout pass, the display pass (in which your drawRect
methods are called), and sending the updated layout and contents to the window server.
Calling layoutIfNeeded
performs layout, if needed, but doesn't invoke the display pass or send anything to the window server. If you want iOS to send updates to the window server, you must commit the current transaction.
One way to do that is to call CATransaction.flush()
. A reasonable case to use CATransaction.flush()
is when you want to put a new CALayer
on the screen and you want it to have an animation immediately. The new CALayer
won't be sent to the window server until the transaction is committed, and you can't add animations to it until it's on the screen. So, you add the layer to your layer hierarchy, call CATransaction.flush()
, and then add the animation to the layer.
You can use CATransaction.flush
to get the effect you want. I don't recommend this, but here's the code:
@IBOutlet var stackView: UIStackView!
@IBAction func buttonWasTapped(_ sender: Any) {
stackView.subviews.forEach { $0.removeFromSuperview() }
for _ in 0 ..< 3 {
addSlowSubviewToStack()
CATransaction.flush()
}
}
func addSlowSubviewToStack() {
let view = UIView()
// 300 milliseconds of “work”:
let endTime = CFAbsoluteTimeGetCurrent() + 0.3
while CFAbsoluteTimeGetCurrent() < endTime { }
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: 44).isActive = true
view.backgroundColor = .purple
view.layer.borderColor = UIColor.yellow.cgColor
view.layer.borderWidth = 4
stackView.addArrangedSubview(view)
}
And here's the result:
The problem with the above solution is that it blocks the main thread by calling Thread.sleep
. If your main thread doesn't respond to events, not only does the user get frustrated (because your app isn't responding to her touches), but eventually iOS will decide that the app is hung and kill it.
The better way is simply to schedule the addition of each view when you want it to appear. You claim “it's not engineering”, but you are wrong, and your given reasons make no sense. iOS generally updates the screen every 16⅔ milliseconds (unless your app takes longer than that to handle events). As long as the delay you want is at least that long, you can just schedule a block to be run after the delay to add the next view. If you want a delay of less than 16⅔ milliseconds, you cannot in general have it.
So here's the better, recommended way to add the subviews:
@IBOutlet var betterButton: UIButton!
@IBAction func betterButtonWasTapped(_ sender: Any) {
betterButton.isEnabled = false
stackView.subviews.forEach { $0.removeFromSuperview() }
addViewsIfNeededWithoutBlocking()
}
private func addViewsIfNeededWithoutBlocking() {
guard stackView.arrangedSubviews.count < 3 else {
betterButton.isEnabled = true
return
}
self.addSubviewToStack()
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) {
self.addViewsIfNeededWithoutBlocking()
}
}
func addSubviewToStack() {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.heightAnchor.constraint(equalToConstant: 44).isActive = true
view.backgroundColor = .purple
view.layer.borderColor = UIColor.yellow.cgColor
view.layer.borderWidth = 4
stackView.addArrangedSubview(view)
}
And here's the (identical) result:
Related Topics
How to Detect When Avplayer Video Ends Playing
Nsfetchedresultscontroller V.S. Uilocalizedindexedcollation
How to Stop Multiple Times Method Calling of Didupdatelocations() in iOS
Cannot Put a Google Maps Gmsmapview in a Subview of Main Main View
Switch Cameras with Avcapturesession
Uibutton Fails to Properly Register Touch in Bottom Region of iPhone Screen
iPhone Gps in Background Never Resumes After Pause
How to Create Custom Mkannotationview and Custom Annotation Title and Subtitle
When Should I Use the Various Storage Mechanisms in iOS
Uiscreen Mainscreen Bounds Returning Wrong Size
iOS 9 Safari: Changing an Element to Fixed Position While Scrolling Won't Paint Until Scroll Stops
App Killed by Sigkill When Changing Privacy Settings
How to Mask a Square Image into an Image with Round Corners in iOS
How to Center Align the Cells of a Uicollectionview
iPhone - Draw Transparent Rectangle on Uiview to Reveal View Beneath