Quantcast
Channel: Kodeco | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more
Viewing all articles
Browse latest Browse all 4370

Easier Auto Layout: Coding Constraints in iOS 9

$
0
0
auto layout

Coding Auto Layout just got a lot easier! Read on to find out more.

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. :]

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:

auto layout

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:

auto layout

  • avatarView – cyan: this view will show the chapter image, book title and social media icons.
    (I’ve called it avatarView 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:

auto layout

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.

auto layout

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:

auto layout

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:

  1. Each view is anchored to the leading edge of the main view’s margin guide, giving the X position constraint.
  2. bookTextView is anchored to the bottom of the main view; chapterLabel is anchored to the top of bookTextView and avatarView is anchored to the top of chapterLabel. This gives all views their Y position constraint.
  3. 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.
  4. 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

Auto Layout Tutorial

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:

  1. You first set translatesAutoresizingMaskIntoConstraints to false. 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.
  2. 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
  3. 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.

auto layout

auto layout

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:

  1. Here you set avatarView to use Auto Layout.
  2. You set avatarView‘s top constraint to the view’s top edge.
  3. You set the leading and trailing edges to constrain to the view’s margins instead of the edges of the view.
  4. 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:

auto layout

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:

auto layout

There’s no change to bookTextView because there are currently no tab bars on the screen.

Readable Content Guide

auto layout

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:

auto layout

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:

auto layout

“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:

auto layout

Build and run your app; you’ll see that the cyan-colored avatarView takes the height of 100 from its intrinsic content size:

auto layout

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:

auto layout

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:

auto layout

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:

auto layout

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:

auto layout

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:

auto layout

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:

auto layout

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:

auto layout

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:

  1. You create two variables to hold titleLabel‘s constraints. The first will constrain titleLabel‘s bottom edge to avatarView‘s bottom and the second will center titleLabel in avatarView.
  2. Similarly, the next three variables hold imageView‘s constraints, with the top of imageView constrained to avatarView and the bottom of imageView to the top of titleLabel. imageView will be centered in avatarView.
  3. The last two variables hold constraints for socialMediaView to be right aligned and constrained to the top of avatarView.

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:

auto layout

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:

auto layout

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:

auto layout

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:

auto layout

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:

  1. You set up a conditional that checks the size class.
  2. 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.
  3. You change the axis of socialMediaView to be horizontal for the Regular size.
  4. 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:

auto layout

auto layout

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

auto layout

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:

  1. You calculate the correct aspect ratio for the image.
  2. 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:

auto layout

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:

auto layout

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:

auto layout

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.

auto layout

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:

auto layout

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:

auto layout

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.


Viewing all articles
Browse latest Browse all 4370

Trending Articles