Do you find Auto Layout challenging? Do you find yourself making and unmaking constraints over and over in a never-ending attempt to find the seemingly random correct layout? Do you find it daunting when you have to modify Auto Layout constraints in code?
Never fear! In this Auto Layout tutorial instead of using Interface Builder and storyboards, you’ll create all your constraints in code using layout anchors, a new API delivered as part of iOS 9. Creating constraints in code will lead to a greater understanding of Auto Layout constraint relationships, and even make it easier for you create Auto Layout driven views in Interface Builder.
You’ll learn about:
- iOS 9 layout anchors
- Layout guides
- Intrinsic content size
- The Auto Layout update cycle
- Laying out for different size classes
In short, you’ll learn how to stay afloat with Auto Layout. :]
You’ll be creating an app called Wonderland, a simple Alice in Wonderland story app. Here’s what the app will look like when you’re finished:
Using code to create all the views and constraints, you’ll lay out the app differently using Compact and Regular size classes.
Although the raywenderlich.com team advocates using storyboards whenever possible, sometimes it’s better to use code to create your views. For example, Wonderland contains an avatar profile view that you can use across several apps. It’s easier to reuse code rather than copy Interface Builder views across several apps.
In addition, if you learn how Auto Layout works in code, it’s easier to apply it in Interface Builder, which can become difficult to follow when you’re laying out various size class combinations.
Getting Started
Download the Wonderland starter app and open it up in Xcode.
Note: Use the iPhone 6s Plus simulator for testing unless told otherwise. The 6s Plus is horizontally Compact in portrait and horizontally Regular in landscape, so you can easily test both layouts.
You can see all the size classes and devices here.
Adaptive Layout Tutorial in iOS 9: Getting Started will teach you the basics of size classes and adaptive layout.
If you’re a raywenderlich.com subscriber you can watch a video to learn more about size classes here.
Run the app and you’ll see three colored views:
avatarView
– cyan: this view will show the chapter image, book title and social media icons.
(I’ve called itavatarView
because, even though in this app it may not look much like an avatar or profile view, it will have the standard profile view objects of image view, name and icons. You could use this view across multiple apps.)chapterLabel
– yellow: tells you what the current chapter is.bookTextView
– green: the text of the current chapter.
You can swipe the text view left or right to move to the next and previous chapters.
The three views are properly positioned using the view’s frame property.
But rotate the device (Cmd + left or right arrow), and the views don’t expand correctly to fill the landscape width:
Open ViewController.swift, there’s only one method – viewDidLoad()
– which calls methods to set up the three views.
The setup methods are defined in ViewControllerExtension.swift. These are separated out because you won’t be changing any of this code. You’ll only be working in ViewController.swift and AvatarView.swift.
What is Auto Layout?
Note: Have a brief look at our Auto Layout in Interface Builder tutorial. Even if you don’t plan to use Interface Builder yourself, the tutorial explains many useful concepts.
Auto Layout is a layout system based on constraints. You no longer think about screen objects in terms of their exact size. Only the relationship between the objects is relevant.
The downside of this is that the constraint system is extremely logical, and with one slip in logic your whole layout can come tumbling down.
The advantages are considerable. In Wonderland you will set up relationships between views of varying sizes, and they will automatically grow and shrink as the device rotates because they are part of a layout system.
Working out Constraints
The best way to work out what constraints are required is to make a drawing of the required result. Forget the details; just look at the basic layout and decide on the relative positioning of views:
For each one of these three views you have to provide at least four constraints: X position, Y position, Width and Height.
These are the constraints you’ll create:
- Each view is anchored to the leading edge of the main view’s margin guide, giving the X position constraint.
bookTextView
is anchored to the bottom of the main view;chapterLabel
is anchored to the top ofbookTextView
andavatarView
is anchored to the top ofchapterLabel
. This gives all views their Y position constraint.- Each view is anchored to the trailing edge of the main view’s margin guide, giving the Width constraint for each – the width essentially stretches from the leading edge to the trailing edge.
- The Height constraint is the most difficult of the constraints to work out, as each view should stretch differently.
The most important of these views is
bookTextView
, which is the text of the current chapter. You’ll set up this view to take up two thirds of the screen, and the other two views will take up the rest of the space.avatarView
is the least important; this can grow and shrink depending on what space is available to it.chapterLabel
should remain the same height no matter what, and is set by the font height of the text.
So now to translate this into code.
First, in ViewController
create a new method called setupConstraints()
:
func setupConstraints() { } |
You’ll be using setupConstraints()
to set up the constraints for all three views.
In viewDidLoad()
remove the call to setupFrames()
and replace it with the following:
setupConstraints() |
Note: Constraints must be added after adding views to their superviews. All constraints need to be in the same hierarchy. If they aren’t, the app will immediately crash.
Constraint Anchors
iOS 9 introduced NSLayoutAnchor
with anchor properties on UIView
. You can now set up your views to be ‘anchored’ to other views.
There are three subclasses of NSLayoutAnchor
:
NSLayoutXAxisAnchor
NSLayoutYAxisAnchor
NSLayoutDimension
When you set up a view’s anchor to be constrained to another view’s anchor, it must be of the same subclass. For example, the compiler will reject constraining a leadingAnchor
, a subclass of NSLayoutXAxisAnchor
, to a heightAnchor
which is a subclass of NSLayoutYAxisAnchor
.
This is another advantage of using anchors: you get extra constraint validity checking for free.
Note: Beware constraining leading / trailing anchors to left / right anchors – this will crash at runtime. Even though they are all instances of NSLayoutXAxisAnchor
, Auto Layout does not let you mix leading with left anchors or trailing with right anchors.
Add the following code to setupConstraints()
:
// 1 bookTextView.translatesAutoresizingMaskIntoConstraints = false // 2 bookTextView.leadingAnchor.constraintEqualToAnchor( view.leadingAnchor).active = true bookTextView.trailingAnchor.constraintEqualToAnchor( view.trailingAnchor).active = true bookTextView.bottomAnchor.constraintEqualToAnchor( view.bottomAnchor, constant: -20).active = true // 3 bookTextView.heightAnchor.constraintEqualToAnchor( view.heightAnchor, multiplier: 0.65).active = true |
Going through this step-by-step:
- You first set
translatesAutoresizingMaskIntoConstraints
tofalse
. This tells the view that it’s using Auto Layout rather than frames. Interface Builder does this automatically, but if you are adding constraints in code, you need to set this property. - You set up
bookTextView
‘s leading, trailing, and bottom anchors to anchor to the main view. The bottom anchor has a constant of -20 which will give a white space margin of 20 points at the bottom of the screen - Using a multiplier, you set
bookTextView
to be always 65% of the height of the view controller’s view, no matter what size that view is.
By setting each constraint’s active property to true
, these constraints are immediately active.
Note: When you set bookTextView
‘s constraints, you used leading and trailing anchors rather than left and right anchors. Leading and trailing become relevant for right-to-left languages such as Arabic. In a right-to-left language, trailing becomes left and leading becomes right.
So when you localize Wonderland to Hebrew or Arabic, there will be no structural layout changes required!
Build and run the app in both portrait and landscape. bookTextView
‘s frame adjusts automatically to match the view controller view’s frame.
You can try this out on the iPad simulators too; the view will always be 65% high.
View Layout Margins
All views have a layoutMarginsGuide
property to which you can anchor, instead of anchoring to the view.
In most cases, rather than extending subviews all the way to the edge of the screen, it’s better to anchor subviews to the left and right view margin guides to allow standard white space around the edges.
You’ll now add avatarView
to the top of the screen using the main view’s left and right layout margin guide anchors. This will leave white space on either side of avatarView
.
Add the following code to the end of setupConstraints()
:
// 1 avatarView.translatesAutoresizingMaskIntoConstraints = false // 2 avatarView.topAnchor.constraintEqualToAnchor( view.topAnchor).active = true // 3 avatarView.leadingAnchor.constraintEqualToAnchor( view.layoutMarginsGuide.leadingAnchor).active = true avatarView.trailingAnchor.constraintEqualToAnchor( view.layoutMarginsGuide.trailingAnchor).active = true // 4 avatarView.heightAnchor.constraintEqualToConstant(200).active = true |
Here’s the explanation of the code above:
- Here you set
avatarView
to use Auto Layout. - You set
avatarView
‘s top constraint to the view’s top edge. - You set the leading and trailing edges to constrain to the view’s margins instead of the edges of the view.
avatarView
‘s height is a constant 200. You’ll be changing this later in this Auto Layout tutorial.
Build and run the app; avatarView
is the cyan view at the top of the screen:
View Controller Layout Guides
As well as the view’s margin guides, view controllers have a top and bottom layout guide.
You can see that avatarView
is underneath the status bar. If you had other translucent bars, such as a navigation or tab bar, the view controller’s view would also extend under these, and the content would be obscured by the bar.
When constraining subviews to a view controller’s view, you should always constrain to the view controller’s top guide’s bottom anchor and bottom guide’s top anchor, rather than the view’s top and bottom anchors. This will prevent the status bar from covering the subview.
Change avatarView
‘s top anchor constraint in setupConstraints()
to:
avatarView.topAnchor.constraintEqualToAnchor( topLayoutGuide.bottomAnchor).active = true |
Here you constrain avatarView
to the view controller’s top layout guide’s bottom anchor. The status bar, and any other translucent bars that may be added later, will no longer cover avatarView
.
Similarly in setupConstraints()
, change bookTextView
‘s bottom anchor to:
bookTextView.bottomAnchor.constraintEqualToAnchor( bottomLayoutGuide.topAnchor, constant: -20).active = true |
This will constrain bookTextView
to the view controller’s bottom layout guide’s top anchor with a margin of 20 points. If you later add a tab bar to your app, then the text will not be covered by the bar.
Build and run the app; avatarView
is no longer covered by the status bar:
There’s no change to bookTextView
because there are currently no tab bars on the screen.
Readable Content Guide
The text in bookTextView
currently goes from edge to edge and is very uncomfortable to read on an iPad’s wide screen.
Using readable content guides, which change depending on size class, you can make the text more readable by automatically adding more white space at the edges than layout margins currently provide.
On the iPhone 6s Plus in portrait, readable content guides are the same as the view’s margin guides, but in landscape there is more white space on either side of the text view. On the iPad in landscape, the white space is increased significantly.
The margin size depends on the system’s dynamic type. The larger the font, the wider the guide will be.
In setupConstraints()
, change bookTextView
‘s leading and trailing anchors to:
bookTextView.leadingAnchor.constraintEqualToAnchor( view.readableContentGuide.leadingAnchor).active = true bookTextView.trailingAnchor.constraintEqualToAnchor( view.readableContentGuide.trailingAnchor).active = true |
This changes the leading and trailing anchors to constrain to the readable content guide instead of the view’s edges.
Run the app on an iPad in landscape, and see how much more readable the text is when it’s centered in the screen:
Note: You’ve used layout guides provided by Apple, but you can create your own layout guides and constrain views to these guides.
When you create a layout guide, you get most of the anchors available with views. So if you anchor several views to a layout guide and then reposition the guide, all the anchored views will move with the guide.
Layout guides are lighter in resources than UIViews
and let you go even further than stack views with spatial relationships.
Intrinsic Content Size
Now it’s time to work on the chapter label.
At the end of setupConstraints()
add the following:
chapterLabel.translatesAutoresizingMaskIntoConstraints = false chapterLabel.centerXAnchor.constraintEqualToAnchor( view.centerXAnchor).active = true chapterLabel.bottomAnchor.constraintEqualToAnchor( bookTextView.topAnchor).active = true |
Here you set chapterLabel
to use Auto Layout and constrained it to the center of the view. You also constrained chapterLabel
‘s bottom to bookTextView
‘s top.
Build and run the app; chapterLabel
will be colored yellow:
“But wait,” you protest. “I only set two constraints. You told me I had to set four.”
Yes, that is true. However, all views have an intrinsic content size. If that intrinsic content size is set, you don’t have to explicitly create layout constraints for Width and Height.
chapterLabel
‘s intrinsic content size is set by the font and text used.
However, a standard UIView
‘s intrinsic content width and height are set by default to UIViewNoIntrinsicMetric
, which means that there is no size.
To demonstrate this, you’ll now change the constraints for avatarView
. This is a subclass of UIView
, so it has no default intrinsic content size. Just for fun, you’ll set avatarView
to have an intrinsic content height of 100 instead of the current constant height of 200.
Remove the following constant constraint from setupConstraints()
:
avatarView.heightAnchor.constraintEqualToConstant(200).active = true |
avatarView
now has X Position, Y Position and Width constraints, but no Height constraint. If you were to run the app now, avatarView
would not show on the screen at all, as it would have a zero height.
In AvatarView.swift, add the following method:
override func intrinsicContentSize() -> CGSize { return CGSize(width: UIViewNoIntrinsicMetric, height: 100) } |
This sets avatarView
to have no intrinsic width, but to have an intrinsic height of 100.
The constraints now look like this:
Build and run your app; you’ll see that the cyan-colored avatarView
takes the height of 100 from its intrinsic content size:
Note: If you ever need to change the intrinsic content size while the app is running, you can update it using invalidateIntrinsicContentSize()
.
In this case you really want chapterLabel
to stay at its intrinsic height and avatarView
to stretch so that it fills out the rest of the view, with a constant margin between them as in this diagram:
First set the constant white space gap by adding the following to the end of setupConstraints()
in ViewController.swift:
avatarView.bottomAnchor.constraintEqualToAnchor( chapterLabel.topAnchor, constant: -10).active = true |
This will constrain avatarView
‘s bottom anchor to be 10 pixels higher than the top of chapterLabel
‘s top anchor.
Build and run the app:
Uh oh. chapterLabel
has stretched instead of avatarView
!
You’ve set up constraints so that chapterLabel
‘s top is anchored to avatarView
‘s bottom and chapterLabel
‘s bottom is anchored to the text view’s top. But when you don’t set an explicit constraint for Width and Height, the Auto Layout engine has to take intrinsic content size as a guide rather than a rule. In this situation it has given priority to avatarView
‘s intrinsic size over chapterLabel
‘s.
chapterLabel
has consequently stretched vertically in a rather uncomfortable manner:
There are two methods associated with intrinsic content size that give priorities to which views should stretch and compress.
setContentHuggingPriority(_:forAxis:)
takes a priority and an axis to determine how much a view wants to stretch. A high priority means that a view wants to stay the same size. A low priority allows the view to stretch.
setContentCompressionResistancePriority(_:forAxis:)
also takes a priority and an axis. This method determines how much a view wants to shrink. A high priority means that a view tries not to shrink and a low priority means that the view can squish.
Note: Priorities are values between 1 and 1000, where 1000 is the highest. The standard priorities are UILayoutPriorityRequired
= 1000, UILayoutPriorityDefaultHigh
= 750 and UILayoutPriorityDefaultLow
= 250.
In this case you want chapterLabel
to always have the correct height for the label and not stretch at all, so add the following to the bottom of setupConstraints()
:
chapterLabel.setContentHuggingPriority( UILayoutPriorityRequired, forAxis: .Vertical) chapterLabel.setContentCompressionResistancePriority( UILayoutPriorityRequired, forAxis: .Vertical) |
This sets chapterLabel
‘s hugging priority and compression resistance as required for the vertical axis. The layout engine will therefore keep chapterLabel
‘s intrinsic content height when it can.
Build and run the app again; you’ll see that avatarView
stretches to fill the gap, which is exactly what you want:
bookTextView
didn’t shrink or stretch, because it has no intrinsic content size and all four constraints were explicitly set.
A Reusable Hierarchy in Code
Now you’ll work on avatarView
. Just like a generic profile view, this view will have an image, title and social media icons. The views will adjust depending on whether the width is Compact or Regular.
This is how avatarView
will look when completed:
In AvatarView.swift all the subviews are set up and ready to go, so just add this code to the end of setup()
:
addSubview(imageView) addSubview(titleLabel) addSubview(socialMediaView) |
Still in AvatarView
, add a new method named setupConstraints()
:
func setupConstraints() { imageView.translatesAutoresizingMaskIntoConstraints = false titleLabel.translatesAutoresizingMaskIntoConstraints = false socialMediaView.translatesAutoresizingMaskIntoConstraints = false } |
Remember, you need to set translatesAutoresizingMaskIntoConstraints
for every view you want to use with Auto Layout.
Call your new method at the end of willMoveToSuperview(_:)
like so:
setupConstraints() |
Build and run the app; the three subviews are there, sized to their intrinsic sizes, but not constrained to the correct positions:
Note: The social media icons are courtesy of Vicki Wenderlich – you can purchase her art from Game Art Guppy.
Activate Arrays of Constraints
Setting each individual constraint’s active property as you have been doing so far is not as efficient as setting up all the constraints first and activating them all at once. So now you’ll set up the constraints for titleLabel
, imageView
and socialMediaView
in this more efficient way.
You’ll add these constraints in code:
Add the following code to the bottom of setupConstraints()
in AvatarView.swift:
// 1 let labelBottom = titleLabel.bottomAnchor.constraintEqualToAnchor(bottomAnchor) let labelCenterX = titleLabel.centerXAnchor.constraintEqualToAnchor( centerXAnchor) // 2 let imageViewTop = imageView.topAnchor.constraintEqualToAnchor(topAnchor) let imageViewBottom = imageView.bottomAnchor.constraintEqualToAnchor( titleLabel.topAnchor) let imageViewCenterX = imageView.centerXAnchor.constraintEqualToAnchor( centerXAnchor) // 3 let socialMediaTrailing = socialMediaView.trailingAnchor.constraintEqualToAnchor(trailingAnchor) let socialMediaTop = socialMediaView.topAnchor.constraintEqualToAnchor(topAnchor) |
Taking the above code step-by-step:
- You create two variables to hold
titleLabel
‘s constraints. The first will constraintitleLabel
‘s bottom edge toavatarView
‘s bottom and the second will centertitleLabel
inavatarView
. - Similarly, the next three variables hold
imageView
‘s constraints, with the top ofimageView
constrained toavatarView
and the bottom ofimageView
to the top oftitleLabel
.imageView
will be centered inavatarView
. - The last two variables hold constraints for
socialMediaView
to be right aligned and constrained to the top ofavatarView
.
For each of the views, you’ve created constraint variables but the constraints are not yet active.
To activate the constraints all at once, add the following to the end of setupConstraints()
:
NSLayoutConstraint.activateConstraints([ imageViewTop, imageViewBottom, imageViewCenterX, labelBottom, labelCenterX, socialMediaTrailing, socialMediaTop]) |
The constraints are now activated in the array order.
As both imageView
and titleLabel
have intrinsic sizes, you’ll need to set imageView
‘s compression resistance to ensure that imageView
resizes in preference to titleLabel
.
Add the code below to the end of setupConstraints()
:
imageView.setContentCompressionResistancePriority( UILayoutPriorityDefaultLow, forAxis: .Vertical) imageView.setContentCompressionResistancePriority( UILayoutPriorityDefaultLow, forAxis: .Horizontal) |
Here you set imageView
‘s compression resistance priority to low for the vertical and horizontal axes.
Finally, change socialMediaView
‘s axis to vertical like so:
socialMediaView.axis = .Vertical |
Build and run on the iPhone 6s Plus and check out the app in portrait:
This layout is looking pretty good. imageView
and titleLabel
are centered, and socialMediaView
is right aligned and vertical.
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.constraintEqualToAnchor( centerXAnchor) |
and
let imageViewCenterX = imageView.centerXAnchor.constraintEqualToAnchor( centerXAnchor) |
and
let socialMediaTop = socialMediaView.topAnchor.constraintEqualToAnchor(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.activateConstraints([ imageViewTop, imageViewBottom, imageViewCenterX, labelBottom, labelCenterX, socialMediaTrailing, socialMediaTop]) |
…with the following:
NSLayoutConstraint.activateConstraints([ 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:
private var regularConstraints = [NSLayoutConstraint]() private var compactConstraints = [NSLayoutConstraint]() |
Add the code below to the end of setupConstraints()
in AvatarView
:
compactConstraints.append( imageView.centerXAnchor.constraintEqualToAnchor(centerXAnchor)) compactConstraints.append( titleLabel.centerXAnchor.constraintEqualToAnchor(centerXAnchor)) compactConstraints.append( socialMediaView.topAnchor.constraintEqualToAnchor(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.constraintEqualToAnchor(leadingAnchor)) regularConstraints.append( titleLabel.leadingAnchor.constraintEqualToAnchor( imageView.leadingAnchor)) regularConstraints.append( socialMediaView.bottomAnchor.constraintEqualToAnchor(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.deactivateConstraints(compactConstraints) NSLayoutConstraint.activateConstraints(regularConstraints) // 3 socialMediaView.axis = .Horizontal } else { // 4 NSLayoutConstraint.deactivateConstraints(regularConstraints) NSLayoutConstraint.activateConstraints(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 6s 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
drawRect(_:)
. 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:
private 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?.active = false aspectRatioConstraint = imageView.widthAnchor.constraintEqualToAnchor( imageView.heightAnchor, multiplier: aspectRatio) aspectRatioConstraint?.active = 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 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.
Laying Out Views Manually
Occasionally you’ll want to access a view’s frame. This can only safely be done in layoutSubviews()
after all the views have been laid out by the Auto Layout engine.
If you run Wonderland on a smaller device such as the iPhone 4s or 5s, there are two problems. Firstly imageView
in landscape is really tiny, and secondly socialMediaView
is too large and draws over text.
To fix this, you’ll check the size of imageView
‘s frame and if it’s below a certain threshold, you’ll hide imageView
. You’ll also check that socialMediaView
fits within avatarView
‘s bounds and if not hide socialMediaView
.
Override layoutSubviews()
in AvatarView
:
override func layoutSubviews() { super.layoutSubviews() if bounds.height < socialMediaView.bounds.height { socialMediaView.alpha = 0 } else { socialMediaView.alpha = 1 } if imageView.bounds.height < 30 { imageView.alpha = 0 } else { imageView.alpha = 1 } } |
Here you set socialMediaView
to be transparent if avatarView
‘s height is less than socialMediaView
‘s height. You also set imageView
to be transparent if its height is less than 30 points. The image is too small to see at this size.
Build and run, and imageView
is hidden if it’s too small and socialMediaView
is hidden if it’s too big. This is an iPhone 4s:
Cleaning Up
Congratulations! You’ve now finished Wonderland, so you can remove all the background colors.
In ViewController.swift, remove the following line from viewDidLoad()
:
colorViews() |
In AvatarView.swift, remove the following lines from setup()
:
imageView.backgroundColor = UIColor.magentaColor() titleLabel.backgroundColor = UIColor.orangeColor() |
Make sure you run the app on various iPhone and iPad simulators in both portrait and landscape to see the resulting effect:
Note: The best transformation happens when you run in landscape on the iPad Air 2 simulator. This allows for multitasking, so if you pull in another application by swiping at the right edge, your size class changes instantly from Regular to Compact. Neat!
Where to Go From Here?
You can download the final project here.
The best resource for understanding Auto Layout is Apple’s WWDC 2015 video Mysteries of Auto Layout 2.
The app demonstrated in that WWDC video is AstroLayout. Although this is in Objective-C, it is well documented. It describes layout guides and constraint animation with keyframes.
There’s a whole guide to Auto Layout here.
Finally, there’s the excellent Ray Wenderlich Auto Layout Video Series.
We hope you enjoyed this Auto Layout tutorial, and if you have any questions or comments, please join the forum discussion below!
The post Easier Auto Layout: Coding Constraints in iOS 9 appeared first on Ray Wenderlich.