Auto Layout on OS X
Over the years, AppKit has been improved and extended in many ways. In OS X Lion, Apple introduced Auto Layout, providing a more flexible and powerful way to position elements on screen.
Unfortunately, as it regularly happens on OS X, the need for maintaining backwards compatibility has lead to important differences and unexpected behaviour in the exact same API when compared to iOS.
Auto Layout
Before we start, let's take a look at what exactly Auto Layout is and how it works. Up until its introduction, views and layers were positioned manually by developers (by overriding appropriate methods) or by setting up an autoresizing mask and letting the framework do the work. In both cases, the positioning was done by setting the frame of a view or in the case of a layer, setting the bounds, position and anchor point (CALayer's -setFrame: is just a convenience method that sets all of the primitive properties).
Auto Layout introduces a single new abstraction – NSLayoutConstraint. The official documentation says: "A constraint defines a relationship between two attributes of user interface objects that must be satisfied by the constraint-based layout system". Crucially, the positions of views are now determined by the solution to the set of constraints set up by the developer (the Auto Layout engine uses the Cassowary constraint solver). If you take one thing away from this article, it should be this: the position of a view is a function of the constraint system. Consequently, it does not make sense anymore to treat the position of views (whether view frames or layer positions, etc.) as a writable property, it can be thought of as being read-only because even if you can mutate it, the Auto Layout machinery will just overwrite it during the next layout pass.
Mixed Layout Trees
With the introduction of Auto Layout, the question arises: does every view need to have its position determined by Auto Layout? There's an extremely large amount of code written that does not use Auto Layout, thus the new approach needs to be backwards compatible. More importantly, we need to know what the restrictions are on mixing code that uses Auto Layout and one that does not.
Auto Layout uses a constraint solver to find a solution to a system of linear equalities and inequalities (the set of NSLayoutConstraints) – that solution determines the positions of the views. Fundamentally, any views referenced by constraints must have positions which can be determined by the constraint solver. If that's not the case (i.e., a view referenced by constraints – meaning its position is determined by Auto Layout – does not have a well-defined position), then that view and any views that are directly or indirectly related to it (via the constraints system) would have an undefined position.
Auto Layout Subtrees
In practice, Auto Layout views are usually related to their superview (whether directly or are related to a sibling which in turn is related to their common superview). Consequently, if a view's position is determined by Auto Layout (and assuming all views are transitively related to their superview – which they are in practice), it means that all its ancestors' positions must also be determined by the constraint solver (by design, as described in the paragraph above). As a corollary, we cannot have non-Auto Layout views with Auto-Layout subviews.
You might wonder what happens if you add a view whose position is determined by Auto Layout as a subview of a non-Auto Layout view tree. The corollary tells us that it should not work but if you tried it, you will see that does. What's going on? Actually, the corollary is correct and the system will make changes to the view tree! On iOS, UIKit notices that we are in state which would render the view referenced by constraints as having an undefined position – the solution is to add constraints to all its ancestors, which is exactly what UIKit does. Whether such constraints are generated is governed by -(BOOL)translatesAutoresizingMaskIntoConstraints. Note that the behaviour is slightly different on OS X – AppKit will try to synthesise constraints for all views in the window, rather than just the minimum required set.
Enabling Auto Layout
If Auto Layout is enabled for a particular window, the semantics of -setFrame: for any views that have -translatesAutoresizingMaskIntoConstraints == YES changes to: update the constraints associated with the receiver such that the output position is exactly equal to the input parameter of -setFrame: (remember that under Auto Layout, the position of a view is a function of the constraint system).
Furthermore, one might ask what determines whether Auto Layout is turned on for a window? It gets enabled when there's at least one constraint in the view hierarchy in a window. You might notice that this definition is perfectly backwards compatible, since any old code would not be adding constraints and thus not enabling it by accident (be careful when creating NIBs, as Xcode enables Auto Layout by default which in turn would activate it in any windows where the views are added).
All the Auto Layout checkbox in IB does is determine whether any constraints are generated for the views contained in the NIB (and it will make sure those views have NO for -translatesAutoresizingMaskIntoConstraints, as otherwise conflicting constraints would be created at runtime). The default value of -translatesAutoresizingMaskIntoConstraints is YES for any views created in code and any views in NIBs which do not have the Auto Layout checkbox enabled.
Non-Auto Layout Subtrees
We might have an existing non-Auto Layout view tree that we want to insert as a subtree of an Auto Layout view. From the perspective of the constraint system, those subviews lie outside its scope (as there are no constraints referencing the subtree), thus the state is perfectly valid. The subviews can be positioned either by using an autoresizing mask or by overriding -layoutSubviews. All of this is possible and works correctly on iOS (see Cocoa-Touch-Autolayout-Mixed code sample).
The situation is slightly different on OS X – once Auto Layout is enabled, AppKit insists that the positions of all views be determined by the constraint system, even for views which are not referenced by any constraints. AppKit will flag any such layout as ambiguous, even though it is not in practice - all views part of the constraint system have well-defined positions. I consider this to be a bug, as I cannot see a reason why such hierarchies need to be flagged as ambiguous (in fact, it works just fine on iOS). The good news is that even though the layout is incorrectly deemed ambiguous, everything seems to be working fine.
View Positioning
The output of constraint solver can be thought of as consisting of the positions of all views related by constraints. Auto Layout pushes those positions to the views as follows:
- On iOS, it calls -setCenter: and -setBounds:. As each UIView is a thin wrapper around CALayer, -setCenter: is equivalent to setting the -anchorPoint to (0.5,0.5) and setting the -position to the given point. UIView's -setBounds: is a pass-through to CALayer's -setBounds:.
- On OS X, it calls -setFrame: on the NSView.
While the difference seems small, it has a very important implication. The reason why -setFrame: is used on OS X is due to backwards compatibility, as a -center property does not (necessarily!) exist at the NSView level (it exists only when layer-based). But due to the introduction of Core Animation in a backwards compatible way, it means that when we have a layer-based view tree, the positions are ultimately stored by the CALayers. And crucially, calling -setFrame: on a CALayer is semantically different to calling -setBounds:, -setPosition: and -setAnchorPoint:.
CALayers do not really have a frame property, as frame is a function of bounds.size, anchorPoint, position and transform. So when Auto Layout sets the frame of the layer, it also resets the transform to the identity. This is an extremely important behavioural difference that needs repeating: Auto Layout on OS X resets -transform of the the auto laid-out CALayers, essentially turning it into a read-only constant. Obviously, this does not happen on iOS as -setFrame: is not used. This behaviour prevents us from using the transform property – e.g., we might want to scale or rotate views but Auto Layout will be overwriting the changes we apply.
A possible workaround would be to apply the transform to the presentation layer, rather than the model layer. We can do this by applying an infinite "animation" with a constant value, which would always overwrite any model layer value. Unfortunately, while this works for static values, there seem to be some graphical glitches if we continually change that value (which requires removing and adding a new animation).
There is a simple workaround we can use to enforce our own transform value – all we need to do is ensure that every time Auto Layout overwrites it, we restore it to what it should be. In practice, this means that we need to override -layout in the superview, call -[super layout] and set the transform to the value we desire.
Mismatched Behaviours
Turning on layer-backing for a particular view tree means that a CALayer will be used to display each NSView. Normally, NSView's -layout is called as part of Auto Layout but it will also be called in other circumstances when using layer-backing. I have not been able to determine whether that's defined behaviour or not (WWDC 2012, Session 217 says: "Works when using auto layout and/or using layer backing", in the context of -updateLayer). In practicle, if a view tree is layer-backed and Auto Layout is off, -layout for a particular NSView will be called if and only if (-wantsUpdateLayer == YES) or (-wantsUpdateLayer and -drawRect are not overridden).
The above behaviour allows us the utilise the lazy -layout method to perform layout related tasks (e.g., dynamically bringing views on screen as a response to scrolling to previously invisible areas). It's very important to note that in this particular context, we are not using Auto Layout. There seems to be a bug on OS X in this particular case – calling -layoutIfNeeded will force enable Auto Layout, even if we have not added any constraints. This might be caused due to -layoutIfNeeded having an undefined behaviour under non-Auto Layout, thus AppKit is free to do whatever it wants. On the other hand, calling -layoutIfNeeded on iOS does not force enable Auto Layout (as -layoutIfNeeded existed before Auto Layout was introduced). Again, backwards compatibility seems to be causing very different behaviour of exactly the same API.
Feedback & Sample Code
The sample code is available on GitHub. Note that at the time of writing, there is a bug on Yosemite which prevents visualisation of constraints. If you have any questions, feedback or you found errors, feel free to contact me over email or Twitter.
Finally, the introduction of Swift presents a unique opportunity for Apple to create a Swift-only AppKit 2.0 which is not crippled by decades of backwards compatibility. We can only hope.
Further Reading
- Auto Layout Guide
- Cocoa Layout Release Notes
- Cassowary Constraint Solver
- 10 Things You Need To Know About Cocoa Auto Layout
- Advanced Auto Layout Toolbox
- Cassowary, Cocoa Autolayout, and enaml constraints
- (WWDC 2011 - Session 103) Cocoa Autolayout
- (WWDC 2012 - Session 202) Intro to Auto Layout for iOS and OS X
- (WWDC 2012 - Session 228) Best Practices for Mastering Auto Layout
- (WWDC 2012 - Session 232) Auto Layout by Example
- (WWDC 2013 - Session 406) Taking Control of Auto Layout in Xcode 5