Demystifying iOS Layout

Some of the most difficult issues to avoid or debug when you first start building iOS applications are those dealing with view layout and content. Often, these issues happen because of misconceptions about when view updates actually occur. Understanding how and when a view updates requires a deeper understanding of the main run loop of […]

Some of the most difficult issues to avoid or debug when you first start building iOS applications are those dealing with view layout and content. Often, these issues happen because of misconceptions about when view updates actually occur. Understanding how and when a view updates requires a deeper understanding of the main run loop of an iOS application and how it relates to some of the methods provided by UIView. This blog post will explain these interactions, hopefully clarifying how to use use UIView’s methods to get the behavior you want.

Main run loop of an iOS app

The main run loop of an iOS application is what handles all user input events and triggers the appropriate responses in your application. Any user interaction with the application is added to an event queue. The application object, shown in the diagram below, takes events off the queue and dispatches them to the other objects in the application. It essentially executes the run loop by interpreting input events from the user and calling the corresponding handlers for that input in the application’s core objects. These handlers call code written by application developers. Once these method calls return, control returns to the main run loop and the update cycle begins. The update cycle is responsible for laying out and redrawing views (described in the next section). Below is an illustration of how the application communicates with the device and processes user input.

Main Event Loop
https://developer.apple.com/library/content/documentation/General/Conceptual/Devpedia-CocoaApp/MainEventLoop.html

Update cycle

The update cycle is the point at which control returns to the main run loop after the app finishes running all your event handling code. It’s at this point that the system begins updating layout, display, and constraints. If you request a change in a view while it is processing event handlers, the system will mark the view as needing a redraw. At the next update cycle, the system will execute all changes on these views. The lag between a user interaction and the layout update should be imperceptible to the user. iOS applications typically animate at 60 fps, meaning that one refresh cycle takes just 1/60 of a second. Because of how quickly this happens, users do not notice a lag in the UI between interacting with applications on their devices and seeing the contents and layout update. However, since there is an interval between when events are processed and when the corresponding views are redrawn, the views may not be updated in the way you want at certain points during the run loop. If you have any computations that depend on the view’s latest content or layout, you risk operating on stale information about the view. Understanding the run loop, update cycle, and certain UIView methods can help avoid or debug this class of issues.

You can see in the diagram below how the update cycle occurs at the end of the run loop.

Update Cycle

Layout

A view’s layout refers to its size and position on the screen. Every view has a frame that defines where it exists on the superview’s coordinate system and how large it is. UIView provides methods that let you notify the system that a view’s layout has changed as well as gives you methods you can override to define actions to take after a view’s layout has been recalculated.

layoutSubviews()

This UIView method handles repositioning and resizing a view and all its subviews. It gives the current view and every subview a location and size. This method is expensive because it acts on all subviews of a view and calls their corresponding layoutSubviews methods. The system calls this method whenever it needs to recalculate the frames of views, so you should override it when you want to set frames and specify positioning and sizing. However, you should never call this explicitly when your view hierarchy requires a layout refresh. Instead, there are multiple mechanisms you can use to trigger a layoutSubviews call at different points during the run loop that are much less expensive than calling layoutSubviews itself.

When layoutSubviews completes, a call to viewDidLayoutSubviews is triggered in the view controller that owns the view. Since layoutSubviews is the only method that is reliably called after a view’s layout is updated, you should put any logic that depends on layout and sizing in viewDidLayoutSubviews and not in viewDidLoad or viewDidAppear. This is the only way you will avoid using stale layout and positioning variables for other computations.

Automatic refresh triggers

There are multiple events that automatically mark a view as having changed its layout, so that layoutSubviews will be called at the next opportunity without the developer doing this manually.

Some automatic ways to signal to the system that a view’s layout has changed are:

  • Resizing a view
  • Adding a subview
  • User scrolling a UIScrollView (layoutSubviews is called on the UIScrollView and its superview)
  • User rotating their device
  • Updating a view’s constraints

These all communicate to the system that a view’s position needs to be recalculated and will automatically lead to an eventual layoutSubviews call. However, there are ways to trigger layoutSubviews directly as well.

setNeedsLayout()

The least expensive way to trigger a layoutSubviews call is calling setNeedsLayout on your view. This will indicate to the system that the view’s layout needs to be recalculated. setNeedsLayout executes and returns immediately and does not actually update views before returning. Instead, the views will update on the next update cycle, when the system calls layoutSubviews on those views and triggers subsequent layoutSubviews calls on all their subviews. There should be no user impact from the delay because, even though there is an arbitrary time interval between when setNeedsLayout returns and when views are redrawn and laid out, it should never be long enough to cause any lag in the application.

layoutIfNeeded()

layoutIfNeeded is another method on UIView that will trigger a layoutSubviews call in the future. Instead of queueing layoutSubviews to run on the next update cycle, however, the system will call layoutSubviews immediately if the view needs a layout update. If you call layoutIfNeeded after calling setNeedsLayout or after one of the automatic refresh triggers described above, layoutSubviews will be called on the view. However, if you call layoutIfNeeded and no action has indicated to the system that the view needs to be refreshed, layoutSubviews will not be called. If you call layoutIfNeeded on a view twice during the same run loop without updating its layout in between, the second call will not trigger a layoutSubviews call.

Using layoutIfNeeded, laying out and redrawing subviews will happen right away and will have completed before this method returns (except in the case where there are in flight animations), unlike setNeedsLayout. This method is useful if you need to rely on the new layout and cannot wait until views are updated on the next update cycle. However, unless this is the case, you should call setNeedsLayout instead and wait for the next update cycle so that you only update views once per run loop.

This method is especially useful when animating changes to constraints. You should call layoutIfNeeded before the start of an animation block to ensure all layout updates are propagated before the start of the animation. Configure your new constraints, then inside the animation block, call layoutIfNeeded again to animate to the new state.

Display

A view’s display encompasses properties of the view that do not involve sizing and positioning of the view and its subviews, including color, text, images, and Core Graphics drawing. The display pass includes similar methods as the layout pass for triggering updates, both those called by the system when it has detected a change, and those we can call manually to trigger a refresh.

draw(_:)

The UIView draw (drawRect in Objective-C) method acts on the view’s contents like layoutSubviews does for the view’s sizing and positioning. However, it does not trigger subsequent draw calls on its subviews. Like layoutSubviews, you should never call draw directly and instead call methods that trigger a draw call at different points during the run loop.

setNeedsDisplay()

This method is the display equivalent of setNeedsLayout. It sets an internal flag that there has been a content update on a view, but returns before actually redrawing the view. Then, on the next update cycle, the system goes through all views that have been marked with this flag and calls draw on them. If you only want to redraw the contents of part of a view during the next update cycle, you can call setNeedsDisplay and pass the rect within the view that needs updating.

Most of the time, updating any UI components on a view will mark the view as “dirty,” by automatically setting the internal “content updated” flag, and cause the view’s contents to be redrawn at the next update cycle without requiring an explicit setNeedsDisplay call. However, if you have any property not directly tied to a UI component but that requires a view redraw on every update, you can define its didSet property observer and call setNeedsDisplay to trigger the appropriate view updates.

Sometimes setting a property requires you to perform custom drawing, in which case you should override draw. In the following example, setting numberOfPoints should trigger the system to draw the view as a shape with the specified number of points. In this case, you should do your custom drawing in draw 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)
        }
    }
}

There is no display method that will trigger an immediate content update on the view, like layoutIfNeeded does with sizing and positioning. It is generally enough to wait until the next update cycle for redrawing views.

Constraints

There are three steps to laying out and redrawing views in Auto Layout. The first step is updating constraints, where the system calculates and sets all the required constraints on the views. Then comes the layout pass, where the layout engine calculates the frames of views and subviews and lays them out. The display pass completes the cycle and redraws views’ contents if necessary by invoking their draw methods, if they have implemented any.

updateConstraints()

This method can be used to enable dynamically changing constraints on a view that uses Auto Layout. Like layoutSubviews() for layout and draw for content, updateConstraints() should only be overridden and never explicitly called in your code. In general, you should only implement constraints that are subject to change in updateConstraints. Static constraints should either be specified in interface builder, in the view’s initializer, or in viewDidLoad().

Generally, activating or deactivating constraints, changing a constraint’s priority or constant value, or removing a view from the view hierarchy will set an internal flag that will trigger an updateConstraints call on the next update cycle. However, there are ways to set the “update constraints” flag explicitly as well, outlined below.

setNeedsUpdateConstraints()

Calling setNeedsUpdateConstraints() will guarantee a constraint update on the next update cycle. It triggers updateConstraints() by marking that one of the view’s constraints has been updated. This method works similarly to setNeedsDisplay() and setNeedsLayout().

updateConstraintsIfNeeded()

This method is the equivalent of layoutIfNeeded, but for views that use Auto Layout. It will check the “constraint update” flag (which can be set automatically, by setNeedsUpdateConstraints, or by invalidateInstrinsicContentSize). If it determines that the constraints need updating, it will trigger updateConstraints() immediately and not wait until the end of the run loop.

invalidateIntrinsicContentSize()

Some views that use Auto Layout have an intrinsicContentSize property, which is the natural size of the view given its contents. The intrinsicContentSize of a view is typically determined by the constraints on the elements it contains but can also be overriden to provide custom behavior. Calling invalidateIntrinsicContentSize() will set a flag indicating the view’s intrinsicContentSize is stale and needs to be recalculated at the next layout pass.

How it all connects

The layout, display, and constraints of views follow very similar patterns in the way they are updated and how to force updates at different points during the run loop. Each component has a method (layoutSubviews, draw, and updateConstraints) that actually propagates the updates, which you can override to manually manipulate views but that you should not call explicitly under any circumstance. This method is only called at the end of the run loop if the view has a flag set that tells the system some component of the view needs to be updated. There are certain actions that will automatically set this flag, but there are also methods that allow you to set it explicitly. For layout and constraint related updates, if you cannot wait until the end of the run loop for these updates (i.e. if other actions are dependent upon the view’s new layout), there are methods you can call to trigger immediate updates, granted the “layout updated” flag is set. Below is a chart that outlines each of these methods as it relates to each component of the UI that may need an update:

Method purposes Layout Display Constraints
Implement updates (override, don’t call explicitly) layoutSubviews draw updateConstraints
Explicitly mark view as needing update on next update cycle setNeedsLayout setNeedsDisplay setNeedsUpdateConstraints
invalidateIntrinsicContentSize
Update immediately if view is marked as “dirty” layoutIfNeeded   updateConstraintsIfNeeded
Actions that implicitly cause views to be updated addSubview
Resizing a view
setFrame that changes a view’s bounds (not just a translation)
User scrolls a UIScrollView
User rotates device
Changes in a view’s bounds Activate/deactivate constraints
Change constraint’s value or priority
Remove view from view hierarchy

The following chart summarizes the interaction between the update cycle and the event loop, and indicates where some of the methods explained above fall during the cycle. You can explicitly call layoutIfNeeded or updateConstraintsIfNeeded at any point in the run loop, keeping in mind that this is potentially expensive. At the end of the loop is the update cycle, which updates constraints, layout, and display if specific “update constraints,” “update layout,” or “needs display” flags are set. Once these updates are complete, the run loop restarts.

Update Cycle
https://i.stack.imgur.com/i9YuN.png

This summary chart and table, and the more granular method explanations above, hopefully clarify the usage of these methods and how each relates to the main iOS run loop. Understanding these methods and how to efficiently trigger the correct updates in your views will allow you to avoid problems with stale layout or content and other unexpected behavior, and debug any issues that do occur.

Source: GameChanger