Applying the Adaptive Layout principles in designing app user interfaces means relying on Auto Layout for sizing and placing the views on screen the right way. With the multiplication of screen sizes, resolutions and devices to support, iOS developers are forced to embrace the Auto Layout technology and abandon the old techniques of manually setting the frames or using the autoresizing masks with their springs and struts.
The Auto Layout system is able to determine the size and position of the views based on a set of constraints defined in code and / or in Interface Builder. Basically, the Auto Layout input is the views to display and the constraints that specify the dimensions of these views and their coordinates relatively to each other. If the input is exhaustive and consistent (there aren’t any missing or incompatible constraints), then Auto Layout outputs the correct frames of the views. But sometimes things can go wrong, especially when dealing with complex layouts involving a large number of views and constraints. Several tools exist to find and fix the Auto Layout issues in these situations.
Constraints can be created and attached to the views either in code (using the NSLayoutConstraint class methods) or in Interface Builder.
There always is a minimal set of constraints to specify for the Auto Layout system to be able to fully determine the views frames. If the developer-defined constraints are insufficient, Auto Layout tries to figure out the missing constraints and creates them automatically. In addition to the NSLayoutConstraint instances created by the developer, there are two types of automatically generated constraints that can be found by inspecting the view hierarchy at runtime: ‚NSAutoresizingMaskLayoutConstraint (constraints created for the views which frames are defined manually or using the springs and struts) and NSIBPrototypingLayoutConstraint (constraints that are inferred by Auto Layout based on the position and size of the views in the storyboard at development time).
Usually the constraints created automatically lead to unexpected results: either the views frames are not set as the developer intended, or the application simply crashes because it is not able to determine the frames for all the views in thee hierarchy. For deterministic and predictable results, all the necessary constraints should be defined by the developer in code, in Interface Builder or in both of them; this eliminates any guess work from Auto Layout and preserves the mental health. If some of the constraints can only be determined at runtime, to prevent Xcode from generating Auto Layout errors and compilation warnings because of the missing constraints, it’s possible to create placeholder constraints in Interface Builder, which are automatically removed at runtime (see the Adaptive Layout – Part 2: Working with Interface Builder article for more details about the placeholder constraints).
As a general good practice, the number of constraints should be as small as possible. Only the necessary and sufficient constraints should be defined in order to optimize performance and simplify the debugging process.
If you used Auto Layout in your apps, you know that the log messages it dumps in the console aren’t very easy to decode.
A typical console message for a NSLayoutConstraint object looks like this:
<NSLayoutConstraint:0x7fc3795830b0 V:|-(200@251)-[UIView:0x7fc379579930] priority:251 (Names: '|':UIView:0x7fc379577a50 )>
These messages are returned by the description property of the NSLayoutConstraint objects.
To log the constraints attached to a specific view, simply loop through the view’s constraints array and print the description of each constraint in the console using NSLog.
Be careful though, because not all the constraints related to a specific view are attached to the view itself; the constraints between the view and any of its superviews or sibling views are attached to a superview. It’s sometimes necessary to recursively run trough the view hierarchy and generate log messages for each view’s constraints.
To log the constraints applied to a specific view, attached to the view itself or to a superview, you should use the UIView method -constraintsAffectingLayoutForAxis:. However, as explained in Apple’s documentation this method isn’t guaranteed to return all the constraints applied to the view.
The constraint and the views it is attached to are identified by their memory addresses which makes it very hard to understand what views on the screen are actually referenced. The message uses a kind of visual language format to suggest that the top of the 0x7fc379579930 view is vertically pinned at 200 points below the 0x7fc379577a50 view.
The NSLayoutConstraint class exposes the identifier instance property which can be used to assign a meaningful description to the constraint and immediately understand which one it is and on which views it operates when viewing the log message:
<NSLayoutConstraint:0x7fa201c4bde0 'Horizontal alignment of the article title in the superview' UIView:0x7fa201c416e0.centerX == UIView:0x7fa201c42e60.centerX>
If the NSLayoutConstraint is instantiated in code, the identifier property can be initialized right away. If it is created in Interface Builder, it can be accessed using an IBOutlet created by Ctrl + clicking on the constraint in the storyboard and dragging to the code file displayed in the Assistant Editor.
But the previous message doesn’t specify exactly which of the two referenced views is the title view and which is its superview.
Fortunately, in development mode it is possible to call the private -_autolayoutTrace method on any UIView instance at it will dump the entire view hierarchy in the console log, from the root window down to the last subview, in a legible format:
| | UINavigationTransitionView:0x7fa201d52f30
| | | UIViewControllerWrapperView:0x7fa201c51070
| | | | •UIView:0x7fa201c416e0
| | | | | *UIView:0x7fa201c42e60
| | | | | *UIButton:0x7fa201c477d0'Test button'
| | | | | | UIButtonLabel:0x7fa201c51760'Test button'
| | | | | *_UILayoutGuide:0x7fa201c433c0
| | | | | *_UILayoutGuide:0x7fa201c50b80
| | UINavigationBar:0x7fa201d3ad70
| | | _UINavigationBarBackground:0x7fa201d447a0
| | | | _UIBackdropView:0x7fa201d5cfe0
| | | | | _UIBackdropEffectView:0x7fa201d61940
| | | | | UIView:0x7fa201d62ed0
| | | | UIImageView:0x7fa201d44de0
| | | UINavigationButton:0x7fa201d3b490'Next'
| | | | UIButtonLabel:0x7fa201d3f9c0'Next'
| | | _UINavigationBarBackIndicatorView:0x7fa201d520f0
It appears that the title view address is 0x7fa201c42e60 and its superview address is 0x7fa201c416e0.
The _autolayoutTrace method is a private API and should never be used in production code. I avoid calling it directly from the app code; when I need to used it, I set a breakpoint in the app after the view hierarchy is completely laid out, and I call the method in the console using this generic syntax:
(lldb) po [[[UIApplication sharedApplication] keyWindow] _autolayoutTrace]
There are two types of Auto Layout issues: the ones that crash the application at runtime, and the ones that lead to unexpected layouts (invisible or partially visible views, wrong size or misplaced views, etc).
Auto Layout is a quite intelligent system and, when the set of constraints provided by the developer is incomplete or conflicting, it first tries to remedy the problems itself by removing unnecessary constraints and / or adding the constraints it thinks are missing. Sometimes it is able to come up with the solution, which in most cases doesn’t match the layout expected by the developer, and other times it has no choice but to crash the application.
In both cases, it fills the console with more or less comprehensible log messages.
The most common causes for Auto Layout issues are:
- forgetting to set the translatesAutoresizingMaskIntoConstraints property to NO for the UIView objects created and added to the view hierarchy in code. By default, an UIView object created in code doesn’t comply with Auto Layout and it expects its frame to be set either directly by specifying the origin and size, or using the autoresizing mask. To force it to respond to Auto Layout constraints, its translatesAutoresizingMaskIntoConstraints property must be set to NO. This property is automatically set to NO for the views added in Interface Builder; if the constraints for these views are not specified by the developer, they are automatically generated at runtime as instances of NSAutoresizingMaskLayoutConstraint class.
- ambiguous constraints, which means that the constraints defined at development time are not sufficient to precisely determine the frames for each view in the hierarchy. If the layout is created in Interface Builder, the ambiguous constraints appear as orange lines in the storyboard. Warning messages like Ambiguous Layout: Size and vertical position are ambiguous for “View” or Ambiguous Layout: Position and size are ambiguous for “View” are also displayed in the Issue navigator. Usually, at runtime, Auto Layout creates constraints on the fly to fix this kind of issues, but the results are often unexpected. Placeholder constraints can be created to avoid the ambiguous layout at development time, but they should be replaced with real constraints at runtime.
- conflicting constraints, which means there are at least two defined constraints that contradict each other and Auto Layout is not able to choose between them (for example, the horizontal spacing between two views is simultaneously set to be greater than 50 points and less than 40 points by two required priority constraints). The conflicting constraints appear in red in Interface Builder. Auto Layout tries to solve these issues at runtime by removing some of the conflicting constraints, but it usually finishes by crashing the application because it cannot correctly determine the frames for all the views. The solution for this kind of issues is to remove the opposing constraints or to set their priorities in such way that Auto Layout can choose only one of them in any specific situation.
There are two instance methods defined in the UIView class that can help identify at runtime the views affected by ambiguous layout: -hasAmbiguousLayout and -exerciseAmbiguityInLayout. The first can be used by recursively running through the view hierarchy and logging the views that have ambiguous layout. The second can be invoked on a view with ambiguous layout and the system will update its frame to match different possible layout solutions (I must admit I wasn’t able to make it work as described in the documentation and I don’t think it’s very useful for debugging).
Using Auto Layout isn’t an easy task and the more complex the view hierarchy, the more issues can occur during implementation or after the release. Because is becomes more and more difficult to get away without using Auto Layout and even the most experienced developers run into problems while using it, understanding how it operates, what are the tools for identifying the issues and how to interpret the messages these tools generate, is essential in creating quality layout code.