UIKit rendering - Tracking a layout phase
On iOS, the layout of a view hierarchy is expressed dynamically using the Auto Layout constraint system. Thus a view hierarchy can adapt itself to all the possible device screens sold by Apple. Auto Layout takes care to translate all the constraints into equations where the positions and the sizes of the views are the variables. As a consequence the rendering of a view hierarchy requires a calculation before each screen refresh.
Let’s find out when the equations solving takes place.
The layout phase
In the life cycle of an application, Apple calls these equations solving moments “layout passes” without ever formally describing them. They were mentioned during a WWDC 2018 talk which specified the order and objectives of the different calculations.
To avoid repetitive and useless calculations, the layout phases always appear after the execution of our code. Whether it is following a touch, a network call or the appearance of a view controller, the modification of a constraint affects the position or the size of its associated view only later.
Therefore we need to have a better understanding of the iOS application code execution path to find out the timing of the layout computation. How does Auto Layout manage to insert the layout computation at the right time and in such an efficient way?
Let’s start by tracking a layout phase!
Tracking a layout phase
The layoutSubviews method
Apple discourages us to call the method
layoutSubviews directly. The UIKit library calls it for us during a layout phase.
Its role is to give to each view its right size and position. It is in this method that the equations defined by our constraints are solved.
During a layout phase, UIKit calls successively (not recursively), and from top to bottom,
layoutSubviews on each of the views that compose the current view tree. At the end of the chain execution, each view has its final position and size.
layoutSubviews allows us to take part of the view layout calculations and modifing the default implementation. This is also the right time to modify the elements that depend on the size of the current view.
Triggering a layout phase
UIKit is not the only one to be able to trigger layout calculations. The
layoutIfNeeded method allows to explicitly trigger a layout phase at any time. We thus get the position of the views that we manipulate during the execution of our code:
We usually use it to animate changes made by a constraint:
Indeed, as a layout phase triggers successive calls to the
layoutSubviews of the views on screen, the size and position of those views are modified in the animation block. The resulting animation mechanisms are the same as those that would have been triggered if the view had been explicitly modified:
Catching an implicit layout phase
Our first goal will be to intercept a call to a
layoutSubviews method implicitly triggered by UIKit. Indeed, we don’t need to call
layoutIfNeeded every time we modify a constraint, someone or something is calling it for us. If we manage to do so, all we have to do is to determine when this call is performed to answer our initial question: when does a layout phase take place?
Let’s start with the following sample code and two breakpoints A and B:
We added a button and a sample view to a
UIViewController view in a xib and we connected them using an
IBAction and an
IBOutlet. We also added a reference to a height constraint attached the sample view.
When the button is pressed, the height constraint of the sample view is modified. The stack of our first breakpoint A looks like:
By continuing the execution, the breakpoint B placed in the
layoutSubviews of the sample view is triggered. By modifying the constraint, UIKit has been forced to trigger a layout phase to satisfy our new layout. We are in the presence of an implicit layout phase!
layoutSubviews method, there is no trace of our code. This stack is totally distinct from the one executed when we pressed the button.
The goal is clear: avoiding unnecessary calculations. By running the layout phase on a later and separate stack, UIKit ensures that the layout calculations take place after all modifications that could affect the position of the views have been made.
But where does this stack come from? We could imagine a more complicated scenario: we could modify a constraint in a block in the
dispatchAsync method. How does UIKit manage to always insert the layout phase stack so well?
The causes of a layout phase
Let’s analyse the elements of the stack.
The top of the stack is familiar.
CALayer.layoutSublayers, for example, is a public method available since iOS 2. It is obviously equivalent to
CALayerDelegate is the link between an
UIView and a
CALayer. We guess that the layout of a view involves the layout of its layer.
The lower part of the stack is the one we are really interested in. It contains the first moments of the layout phase.
At the origin of the layout calculation of our view, we find a
commit method of a mysterious
CATransaction class. Below, we see methods related to an enigmatic run loop:
The run loop is an essential component of all iOS applications. It is a safe bet that the success of our exploration depends on its understanding and its connection with the
The steps of our exploration are defined:
- What is a
- What is a run loop?
- How are they linked?
On the next post, we will tackle the first topic: CATransaction.