While developing away on my iCatalog Editor I found what I believe to be the first instance in my career as developer where Auto-Layout actually saves me a lot of work.
Before Auto-Layout you would have to calculate view frames and apply them, usually in a layoutSubviews on iOS. The problem being that it usually takes lots of experimentation to get all the cases right.
In my use case I wanted to create a panel for my Mac app that would dynamically adjust to an optional icon on the left side and an optional cancel button on the right, with a progress bar in between. Auto-Layout (after some initial non-understanding on my part) made this a sinch.
While I am exploring constraints for a Mac app, the exact same methods also apply for iOS development.
The use case is to have a panel that shows progress of a long duration activity. An optional icon depicting the activity visually. And an optional cancel button on the right side that allows the user to reconsider. There is a title label above the progress bar and a progress description label below it. As the progress bar resizes to fill the available space I would want these other labels to resize the same.
Let me show you the four possible cases so that you can imagine what I can talking about:
Of course the icon should automatically resize to the image you set on it. And the labels should also follow the size and position of the progress bar such that they are always exactly centered. It would look weird if they remain centered on the view while the progress bar is shifted to the left or right
Basic Orientation
We can set up the panel in interface builder. There are some things that are cumbersome and non-intuitive. You have to sweat it and discover the workarounds or else you will be tempted multiple times in the process to throw in the the towel.
The first thing to know about is the Auto-Layout Master Switch. This allows you turn constraints on or off for every XIB file. Select the root view in the XIB and find the “Use Auto Layout” checkbox on the leftmost tab. Xcode 4.5 enables this by default.
Layout Constraints can either be implied or explicit. Implied means that they are inferred from the initial frame of the view and you can identify these by their purple color and that they exist even with you having added them.
Explicit constraints, also called User Constraints, are showing in blue. Those you need if you want to reference them from code, because you cannot plug implied constraints into IBOutlets.
Breaking with many other UI paradigms in Interface Builder the controls to work with constraints are hovering over your canvas in the lower right corner.
The right one is the most annoying because it is on by default. Open the “When Resizing Views Apply Constraints to …” menu by clicking on it and disable both options. If this is on then IB will add lots of constraints whenever you resize any view. Very annoying, at least for beginners.
Maybe it gets better as you understand the automatic behavior more, but I am not at that level yet. I prefer to decide myself exactly what constraints I want/need.
The left option is called the “Align” tool, the center one is called “Pin”. You can think of Align as affecting the positioning and Pin the sizing. The latter also indirectly affecting positioning because you can specify a given horizontal distance from an edge of the super view.
Some options are available if only one view is selected. Some require that you select two. You can constrain a view width, but to specify the spacing between two views you have to select both of them first.
Another annoyance relates to the way how you can get to edit a constraint. You’d imagine that you could get there by double-clicking on it, but this only opens an outlet panel. Instead you have to select the view that this constraint is attached to and look for its constraints on the size tab.
Again, double-clicking doesn’t do anything for us. You have to click on the drop down next to the cogwheel and choose Edit. You can also Delete, but only User-Constraints. And you can Promote to User Constraint an implied constraint.
I cannot even begin to pretent that I know what the Content Hugging Priority and the Content Compression Resistance Priority are good for.
Setting up the View
Ok, with the above knowledge we are able to lay out our panel with the necessary elements.
Those are: Two labels, a custom button, a progress bar and the image view for the icon.
The left and right outer margins should always remain at the automatic default width which is also what the elements snap to if you drag them there. That means if the icon does not exist then both the width of the icon as well as the space to the immediate right of it should be zero. Conversely if the cancel button is hidden then it and the space to the left of it should be of zero width.
We need a width constraint for the image view and cancel button. A horizontal space constraint between the icon and the progress bar. And a horizontal space constraint between the progress bar and the cancel button.
To get the labels to always align with the progress bar we align the left side via the alignment tool. Finally we set their widths to equal as well, twice: once the top label and the bar, once the bottom label and the bar.
In order to affect elements in interface builder – which includes User Constraints – we have to have an outlet for them. This is where the assistant editor is of great assistance. You can simply Ctrl-drag any constraint into the header file for public visibility or into a private anonymous category in the implementation file.
Since I want to hide the internals of the panel from the developer I choose the latter, private properties.
All these properties can be weak references because they are retained by the views they are attached to, which are in turn retained by their superviews and the root view is retained by its view controller.
We want to have the header of this highly reusable class as plain and simple as possible hiding all the implementation details.
ProgressPanelController.h
typedef void (^ProgressPanelControllerCancelBlock)(void); @interface ProgressPanelController : NSWindowController /** The message to display above the progress bar */ @property (nonatomic, copy) NSString *title; /** The message to display below the progress bar */ @property (nonatomic, copy) NSString *progressMessage; /** An icon to show on the left side. */ @property (nonatomic, strong) NSImage *icon; /** The progress to show. Range from 0.0 to 1.0. Negative values switch to indeterminate. */ @property (nonatomic, assign) CGFloat progressPercent; /** A cancel handler. The cancel button only shows if this is set. */ @property (nonatomic, copy) ProgressPanelControllerCancelBlock cancelHandler; @end |
Setting the icon property should show that. Setting the cancelHandler should show the cancel button. Otherwise they should be hidden.
In our implementation we need access to four constraints:
@interface ProgressPanelController () @property (weak) IBOutlet NSLayoutConstraint *imageWidthConstraint; @property (weak) IBOutlet NSLayoutConstraint *spaceRightOfIconConstraint; @property (weak) IBOutlet NSLayoutConstraint *spaceLeftOfCancelButtonConstraint; @property (weak) IBOutlet NSLayoutConstraint *cancelButtonWidthConstraint; @end |
When somebody sets the properties we need to update these constraints. Also right after initializing this view controller the view and all outlets have not been loaded yet. Therefore we need to temporarily save them until the view has been created. On Mac that would be the windowDidLoad for a window controller, on iOS a viewDidLoad method.
- (void)windowDidLoad { [super windowDidLoad]; if (_progressMessage) { _progressLabel.stringValue = _progressMessage; } if (_title) { _titleLabel.stringValue = _title; } _imageView.image = _icon; [self _updateConstraints]; } |
Finally – drum roll – here’s the _updateContraints that updates the constraints.
- (void)_updateConstraints { if (_icon) { _imageWidthConstraint.constant = _icon.size.width; _spaceRightOfIconConstraint.constant = 8.0f; } else { _imageWidthConstraint.constant = 0.0f; _spaceRightOfIconConstraint.constant = 0.0f; } if (_cancelHandler) { _cancelButtonWidthConstraint.constant = _cancelButton.image.size.width; _spaceLeftOfCancelButtonConstraint.constant = 8.0f; } else { _cancelButtonWidthConstraint.constant = 0.0f; _spaceLeftOfCancelButtonConstraint.constant = 0.0f; } } |
We are modifying the constant property in all cases. All these constraints are absolute width constraints and setting the constant there changes the width absolutely.
At this point I have to credit Jonathan Willing who pushed me gently over the last hurdle to understanding: you don’t work with frames any more, but instead change the constraints.
@cocoanetics The point is that you no longer change the frame, but you change the constraint constants instead.
— Jonathan Willing (@j_willing) November 29, 2012
That’s all it takes! Auto-Layout will notice the modifications and will go and update the view frames based to the constraint solution it calculates.
Conclusion
It probably seems like Apple has introduced Constraint-based Layout in a way that they prefer nobody to actually use it. But use cases like the one presented here should convince you that the process has merit.
You just have to play with it as much as you can and soon you’ll be constraining layouts like a boss, too.
Categories: Recipes
Thank you for this article, it saved me precious time!!
All the best!
Many thanks for this great article! Made me realize you couldn’t do everything in IB. Sometimes you need to write some code to get what you want. Thanks for explaining this!