Easier Auto Layout: Coding Constraints in iOS 9
iOS 9 made coding Auto Layout constraints far easier! Learn everything you need to know about layout guides and layout anchors in this Auto Layout tutorial. By Caroline Begbie.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Easier Auto Layout: Coding Constraints in iOS 9
35 mins
- Getting Started
- What is Auto Layout?
- Working out Constraints
- Constraint Anchors
- View Layout Margins
- View Controller Layout Guides
- Readable Content Guide
- Intrinsic Content Size
- A Reusable Hierarchy in Code
- Activate Arrays of Constraints
- Arrange Layouts by Size Class
- Constraint Activation and Deactivation
- The Constraint Update Cycle
- Updating constraints
- Laying Out Views Manually
- Cleaning Up
- Where to Go From Here?
Arrange Layouts by Size Class
You’ve now learned all the basics of easy Auto Layout. In this section you’ll put everything together and complete your reusable avatar view to be laid out entirely differently in different size classes:
If the size class is Compact, you want imageView
and titleLabel
to be centered, and the social media icons should be right-aligned but laid out vertically, like this:
If the size class is Regular, imageView
and titleLabel
should be left-aligned and the social media icons should still be right-aligned but laid out horizontally:
Constraint Activation and Deactivation
Many of the constraints will need to be activated and deactivated, so you’ll now set up arrays of constraints, but only activate the array appropriate for the device size class.
To do this, you’ll first remove the constraints that will change for each size class, leaving only the constraints that will not change.
Still in AvatarView.swift, in setupConstraints()
remove the following code:
let labelCenterX = titleLabel.centerXAnchor.constraint(
equalTo: centerXAnchor)
and
let imageViewCenterX =
imageView.centerXAnchor.constraint(
equalTo: centerXAnchor)
and
let socialMediaTop = socialMediaView.topAnchor.constraint(equalTo: topAnchor)
The constraints that remain are the top and bottom constraints for imageView
and titleLabel
and a trailing anchor so that socialMediaView
will always be right-aligned.
Change the activation array to contain the constraints that you’ve set so far. Replace:
NSLayoutConstraint.activate([
imageViewTop, imageViewBottom, imageViewCenterX,
labelBottom, labelCenterX,
socialMediaTrailing, socialMediaTop])
…with the following:
NSLayoutConstraint.activate([
imageViewTop, imageViewBottom,
labelBottom,
socialMediaTrailing])
The constraints you’ve set up here for imageView
, titleLabel
and socialMediaView
are the same for both Compact and Regular size classes. As the constraints won’t change, it’s OK to activate the constraints here.
In AvatarView
, create two array properties to hold the constraints for the different size classes:
fileprivate var regularConstraints = [NSLayoutConstraint]()
fileprivate var compactConstraints = [NSLayoutConstraint]()
Add the code below to the end of setupConstraints()
in AvatarView
:
compactConstraints.append(
imageView.centerXAnchor.constraint(equalTo: centerXAnchor))
compactConstraints.append(
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor))
compactConstraints.append(
socialMediaView.topAnchor.constraint(equalTo: topAnchor))
Here you again set up the constraints that you removed, but these are now in an array that can be activated when the size class is Compact.
Add the following code to the end of setupConstraints()
:
regularConstraints.append(
imageView.leadingAnchor.constraint(equalTo: leadingAnchor))
regularConstraints.append(
titleLabel.leadingAnchor.constraint(
equalTo: imageView.leadingAnchor))
regularConstraints.append(
socialMediaView.bottomAnchor.constraint(equalTo: bottomAnchor))
You’ve now set up, but not yet activated, the constraints that will be used when the device changes to the Regular size class.
Now for the activation of the constraint arrays.
The place to capture trait collection changes is in traitCollectionDidChange(_:)
, so you’ll override this method to activate and deactivate the constraints.
At the end of AvatarView
, add the following method:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
// 1
if traitCollection.horizontalSizeClass == .regular {
// 2
NSLayoutConstraint.deactivate(compactConstraints)
NSLayoutConstraint.activate(regularConstraints)
// 3
socialMediaView.axis = .horizontal
} else {
// 4
NSLayoutConstraint.deactivate(regularConstraints)
NSLayoutConstraint.activate(compactConstraints)
socialMediaView.axis = .vertical
}
}
Here you activate and deactivate the specific arrays for the specific size class.
Going through each numbered comment in turn:
- You set up a conditional that checks the size class.
- If the size class is Regular, you deactivate the constraints in the array for the Compact size class and activate the constraints for the Regular size class.
- You change the axis of
socialMediaView
to be horizontal for the Regular size. - Similarly, you deactivate the Regular size class array and activate the Compact size class array and change the
socialMediaView
to be vertical for the Compact size.
Build and run on the iPhone 7 Plus, and rotate between portrait and landscape to see the final view positions:
Unfortunately, due to the image’s intrinsic content size, the image does not appear to be left-aligned in landscape. However, the image view has a magenta background, so you can see that it really is left-aligned. You’ll sort that out shortly.
The Constraint Update Cycle
This diagram shows how views are drawn. There are three main passes with methods that you can override to update views or constraints once the system has calculated the layout:
- All the constraints are calculated in
updateConstraints()
. This is where all priorities, compression resistance, hugging and intrinsic content size all come together in one complex algorithm. You can override this method to change constraints. - Views are then laid out in
layoutSubviews()
. If you need to access the correct view frame, you can override this. - Finally the view is drawn with
draw(_:)
. You can override this method to draw the view’s content with Core Graphics or UIKit.
When the size class changes due to multitasking or device rotation, view layout updates are automatic, but you can trigger each part of the layout with the setNeeds...()
methods listed on the left of the diagram.
Changing the layout of imageView
is a good example of why you might need to recalculate the layout constraints.
Updating constraints
To fix the horizontal size of imageView
, you’ll need to add an aspect ratio constraint. The height of imageView
is calculated by the constraints you’ve already set up, and the width of the image view should be a percentage of that height.
To complicate matters, the constraint will have to be updated every time the user goes to a new chapter when the image is changed — the aspect ratio will be different for every image.
updateConstraints()
executes whenever the constraint engine recalculates the layout, so this is a great place to put the code.
Create a new property in AvatarView
to hold the aspect ratio constraint:
fileprivate var aspectRatioConstraint:NSLayoutConstraint?
Add the following method to AvatarView
:
override func updateConstraints() {
super.updateConstraints()
// 1
var aspectRatio: CGFloat = 1
if let image = image {
aspectRatio = image.size.width / image.size.height
}
// 2
aspectRatioConstraint?.isActive = false
aspectRatioConstraint =
imageView.widthAnchor.constraint(
equalTo: imageView.heightAnchor,
multiplier: aspectRatio)
aspectRatioConstraint?.isActive = true
}
Taking this step-by-step:
- You calculate the correct aspect ratio for the image.
- Although it looks like you are changing the constraint here, you are actually creating a brand new constraint. You need to deactivate the old one so that you don’t keep adding new constraints to
imageView
. If you were wondering why you created a property to hold the aspect ratio constraint, it was simply so that you would have a handle to the constraint for this deactivation.
Build and run the app; notice that the image view is properly sized as you can’t see any magenta showing from behind the image:
However, you’re not finished yet! Swipe the text view to the left to load Chapter 2. Chapter 2’s image has a completely different aspect ratio, so the dreaded magenta bands appear:
Whenever the image changes, you need a way of calling updateConstraints()
. However, as noted in the diagram above, this is a method used in the Auto Layout engine calculations – which you should never call directly.
Instead, you need to call setNeedsUpdateConstraints()
. This will mark the constraint layout as ‘dirty’ and the engine will recalculate the constraints in the next run loop by calling updateConstraints()
for you.
Change the image property declaration at the top of AvatarView
to the following:
var image: UIImage? {
didSet {
imageView.image = image
setNeedsUpdateConstraints()
}
}
As well as updating imageView
‘s image, this now calls setNeedsUpdateConstraints()
which means that whenever the image property is set, all constraints will be recalculated and updated.
Build and run, swipe left to Chapter 2 and your aspect ratio constraint should work perfectly:
Note: If you hadn’t set imageView
‘s horizontal compression resistance to ‘low’ earlier, the image would not have shrunk properly in the horizontal axis.
Note: If you hadn’t set imageView
‘s horizontal compression resistance to ‘low’ earlier, the image would not have shrunk properly in the horizontal axis.