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.
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.
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.
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.
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 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
viewDidAppear. This is the only way you will avoid using stale layout and positioning variables for other computations.
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:
layoutSubviewsis called on the
UIScrollViewand its superview)
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.
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 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
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.
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.
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.
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
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.
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.
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
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() 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
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.
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.
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 (
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:
|Implement updates (override, don’t call explicitly)||
|Explicitly mark view as needing update on next update cycle||
|Update immediately if view is marked as “dirty”||
|Actions that implicitly cause views to be updated||
Resizing a view
User scrolls a UIScrollView
User rotates device
|Changes in a view’s
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.
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.