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.

Leave a rating/review
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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

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

LayoutCycle

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:

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

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.