Demystifying iOS Layout
One of the hardest things to avoid or debug when you’re first developing an iOS app is layout related issues. Often the cause of this problem is a misunderstanding of when the view is actually updated. Understanding how and when views are updated requires a deep understanding of the iOS RunLoop and related UIView methods. This article will introduce these associations and hopefully help clarify how you can use UIView methods to get the correct behavior.
The main RunLoop for an iOS app
The main RunLoop of an iOS app handles all user input events and triggers responses. All user interactions are added to an event queue. The Application Object in the figure below pulls events from the queue and distributes them to other objects in the Application. It essentially interprets these input events from the user, and then calls the corresponding processing code for Core Objects in the application, which in turn calls the code written by the developer. When these method calls return, the control flow returns to the main RunLoop and the Update cycle begins. Update Cycle is responsible for laying out and re-rendering views (more on that next). The following image shows how the application interacts with the device and processes user input.
Developer.apple.com/library/con…
Update Cycle
Update Cycle is the point in time when the control flow returns to the main RunLoop after the application has completed all of your event handling code. It is at this point in time that the system begins to update the layout, display, and set constraints. If you request changes to a view in the event handler code, the view will be marked as needing to be redrawn. During the subsequent Update cycle, the system performs these changes on the View. Delays between user interactions and layout updates are barely noticeable to users. IOS apps typically display animations at 60 FPS, which means that each update cycle takes 1/60 of a second. The update process is fast, so the user doesn’t feel the update delay in the UI when interacting with the application. However, since there is a gap between the processing of the event and the redrawing of the corresponding view, the view update in the RunLoop at some point may not be what you want it to be. If some calculations in your code depend on the current view content or layout, you run the risk of operating on outdated view information. Understanding the specific methods in RunLoop, Update Cycle, and UIView can help avoid or debug such problems. The figure below shows the Update cycle occurring at the end of the RunLoop.
layout
The layout of a view refers to its size and position on the screen. Each view has a frame property that represents its position and size in the parent view’s coordinate system. UIView gives you methods to notify the system that a view layout has changed, and it also gives you rewritable methods to call after the view layout has been recalculated.
layoutSubviews()
This UIView method handles repositioning and resizing of a view and all its subviews. It is responsible for giving the position and size of the current view and each of its children. This method is expensive because it works on each subview and calls its corresponding layoutSubviews method. The system will call this method anytime it needs to recalculate the frame of the view, so you should reload it when you need to update the frame to reposition or resize it. However, you should not call this method explicitly in your code. Instead, there are many mechanisms that trigger layoutSubviews calls at different points in the run loop, and these trigger mechanisms are much less costly than calling layoutSubviews directly.
When layoutSubviews are complete, the view Controller, the owner of the view, fires the viewDidLayoutSubviews call. Since viewDidLayoutSubviews is the only method that can reliably be called when the view layout is updated, you should put all your layout or size dependent code in viewDidLayoutSubviews, Instead of putting it in viewDidLoad or viewDidAppear. This is the only way to avoid using outdated layout or location variables.
Automatic refresh trigger
There are many events that automatically mark the view with “Update Layout”, so layoutSubviews will be called in the next cycle without requiring the developer to do anything manually. These automatic ways of notifying the system that the layout of a view has changed include:
- Modify the size of the view
- New subview
- The user in
UIScrollView
The rolling (layoutSubviews
Will be inUIScrollView
And its parent view.) - User rotating equipment
- Update the constraints of the view
Each of these methods tells the system that the position of the view needs to be recalculated, which automatically translates into a final layoutSubviews call. Of course, there are ways to trigger layoutSubviews directly.
setNeedsLayout()
The cheapest way to trigger a layoutSubviews call is to call the setNeedsLaylout method on your view. Calling this method indicates to the system that the layout of the view needs to be recalculated. The setNeedsLayout method executes immediately and returns, but does not actually update the view until it returns. Views are updated in the next Update cycle, when the system calls the layoutSubviews method of the views and all their subviews. Even if there is an arbitrary amount of time between the time setNeedsLayout returns and the view is redrawn and laid out, the delay does not affect the user because it is never long enough to cause the interface to stall.
layoutIfNeeded()
LayoutIfNeeded is another method that causes UIView to trigger layoutSubviews. Unlike setNeedsLayout(), which causes the view to call layoutSubviews the next cycle to update the view, layoutIfNeeded immediately calls the layoutSubviews method. However, if you call layoutIfNeeded and there is no action to indicate to the system that the view needs to be refreshed, layoutSubView will not be called. If you call layoutIfNeeded twice in the same Run loop and the view is not updated between calls, the second call also does not trigger the layoutSubviews method.
With layoutIfNeeded, layout and redrawing occur immediately and are completed before the function returns (unless there is an animation in progress). This method is more useful than setNeedsLayout when you need to rely on the new layout and cannot wait for the next update cycle. Unless this is the case, you should use setNeedsLayout so that the layout is updated only once per run loop.
This method is especially useful when you want to animate a constraint by modifying it. You need to call layoutIfNeeded on self.view before the animation block to ensure that all layout updates are propagated before the animation starts. After the new Constrait is set in the animation block, layoutIfNeeded needs to be called again to animate to the new state.
According to
The display of a view contains view properties such as colors, text, images, and Core Graphics drawings, but not the size and position of its own and its subviews. Like layout, display has methods that trigger updates, which are called automatically by the system when an update is detected, or we can call a direct refresh manually.
draw(_:)
UIView’s draw method (Swift is used in this article, corresponding to objective-C drawRect) displays view content, similar to layoutSubviews of view layout, but different from layoutSubviews, The draw method does not trigger subsequent calls to the view’s subview methods. Also, like layoutSubviews, you should not call the draw method directly, but instead call the trigger method, which automatically calls the system at different points in the Run loop.
setNeedsDisplay()
This method is similar to setNeedsLayout in the layout. It sets an internal marker for the updated view, but returns it before the view is redrawn. Then in the next Update cycle, the system iterates through all marked views and invokes their DRAW method. If you only want to redraw part of the view for the next update, you can call setNeedsDisplay(_:) and pass in the rectangle that needs to be redrawn (setNeedsDisplayInRect in OC). Most of the time, updating any UI component in a view marks the corresponding view as “dirty”, and by setting the view’s “internal update mark” it will be redrawn in the next Update cycle without the need for an explicit setNeedsDisplay call. However, if you have a property that is not bound to the UI component but needs to redraw the view every time it is updated, you can define its didSet property and call setNeedsDisplay to trigger the view’s appropriate update.
Sometimes setting a property requires custom drawing, in which case you need to override the draw method. In the example below, setting numberOfPoints triggers the system to draw a view based on the numberOfPoints. In this example, you need to implement custom drawing in the Draw method and call setNeedsDisplay in the Property Observer of numberOfPoints.
class MyView: UIView {
var numberOfPoints = 0 {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
switch numberOfPoints {
case 0:
return
case 1:
drawPoint(rect)
case 2:
drawLine(rect)
case 3:
drawTriangle(rect)
case 4:
drawRectangle(rect)
case 5:
drawPentagon(rect)
default:
drawEllipse(rect)
}
}
}
Copy the code
There is no layoutIfNeeded method in the view’s display method that triggers an immediate update like the layoutIfNeeded method in layout. It is usually ok to wait until the next update cycle to redraw the view.
The constraint
Automatic layout consists of three steps to lay out and redraw the view. The first step is to update the constraints. The system calculates and sets all the required constraints for the view. The second step is the layout phase, where the layout engine calculates the frames of the view and subviews and lays them out. The final step to complete the loop is the display phase, which redraws the contents of the view and calls draw if the draw method is implemented.
updateConstraints()
This method is used to dynamically change view constraints in automatic layout. Like the layoutSubviews() method in a layout or the draw method in a display, updateConstraints() should only be overloaded and should never be called explicitly in code. In general you should only implement constraints that must be updated in the updateConstraints method. Static constraints should be specified in the Interface Builder, view initializer, or viewDidLoad() method.
Typically, setting or unbinding a constraint, changing the priority or constant value of a constraint, or removing a view from the view hierarchy sets an internal flag “Update constarints” that triggers the call to updateconContinent on the next update cycle. Of course, there is a way to manually mark a view with the “Update constarints” label, as follows.
setNeedsUpdateConstraints()
Call setNeedsUpdateConstraints () will ensure that in the next update cycle update constraints. It triggers updateConstraints() by marking “Update constraints”. This method works similarly to the setNeedsDisplay() and setNeedsLayout() methods.
updateConstraintsIfNeeded()
This approach is equivalent to layoutIfNeeded for views that use automatic layout. It will check the update constraints “tag (automatically set can be setNeedsUpdateConstraints or invalidateInstrinsicContentSize method). If it thinks these constraints need to be updated, it triggers updateConstraints() immediately, rather than waiting until the end of the Run loop.
invalidateIntrinsicContentSize()
Some views in automatic layouts have the attribute intrinsicContentSize, which is the view’s natural size based on its content. The intrinsicContentSize of a view is usually determined by the constraints of the elements it contains, but can also be overloaded to provide custom behavior. Call invalidateIntrinsicContentSize () will be set a tag says this view intrinsicContentSize has expired, the need to recalculate the next layout stage.
How are they connected
Layouts, displays, and constraints all follow similar patterns, such as how they are updated and how updates are enforced at different points in the run loop. Each component has an actual update method (layoutSubviews, Draw, and updateConstraints) that you can override to manipulate the view manually, but do not explicitly call it in any case. This method is called only at the end of the Run loop if the view is tagged with a flag telling the system that the view needs to be updated. There are some actions that set this flag automatically, but there are also methods that allow you to explicitly set it. For layout and constraint-related updates, if you can’t wait to update at the end of the Run loop (for example, other actions depend on the new layout), there are ways to update immediately and ensure that the “Update Layout” tag is marked correctly. The table below lists how any component will be updated and how.
The following flowchart summarizes the interaction between the Update Cycle and event loop and indicates where the methods mentioned above are located during the run loop. You can run any point in the loop to explicitly call layoutIfNeeded or updateConstraintsIfNeeded, need to remember that it will cost a lot. At the end of the cycle is the Update cycle, where constraints, layout, and presentation are updated if the view is set to specific “Update constraints”, “Update Layout”, or “needs Display” flags. Once these updates are complete, runloop restarts.