Auto Layout Visual Format Language Tutorial

In this tutorial you will learn how to use the Auto Layout Visual Format Language to easily lay out your app’s user interface using code. By József Vesza.

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

Layout Options

Layout options provide the ability to manipulate view constraints perpendicular to the current layout orientation being defined.

Applying vertical centering to all views in a horizontal layout orientation by using NSLayoutFormatOptions.AlignAllCenterY is an example of layout options.

You wouldn't use this option in vertical orientation since there's no sense in trying to set all of the views' centers vertically while laying them out vertically, edge by edge. It's also not provided for vertical orientation, so there you go.

Next, you'll see how layout options are useful when it comes to constructing layouts. Remove the following code from viewDidLoad():

let nameLabelVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-23-[appNameLabel]",
  metrics: nil,
  views: views)
allConstraints += nameLabelVerticalConstraints
    
let skipButtonVerticalConstraints = NSLayoutConstraint.constraintsWithVisualFormat(
  "V:|-20-[skipButton]",
  metrics: nil,
  views: views)
allConstraints += skipButtonVerticalConstraints

You're just removing the vertical constraints from the appNameLabel and skipButton. Instead, you're going to use the layout options to give them a vertical position.

Find the code that creates topRowHorizontalConstraints and set the options parameter to [.alignAllCenterY]. It should now look like the following:

let topRowHorizontalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "H:|-15-[iconImageView(30)]-[appNameLabel]-[skipButton]-15-|",
  options: [.alignAllCenterY],
  metrics: nil,
  views: views)
allConstraints += topRowHorizontalConstraints

You've provided the NSLayoutFormatOption .alignAllCenterY that takes each view inside the format string and creates layout constraints to align each of them by their vertical centers. This code works since the iconImageView has previously defined vertical layout constraints, including its height. Thus, the appNameLabel and skipButton are vertically centered with the iconImageView.

If you build and run now, the layout will look exactly the same, but you know the code is better :]

Remove the code that creates welcomeHorizontalConstraints and adds it to the constraints array.

This removes the horizontal constraints from the welcomeLabel.

Next, update the options when creating summaryLabelVerticalConstraints definition to the following:

let summaryLabelVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[welcomeLabel]-4-[summaryLabel]",
  options: [.alignAllLeading, .alignAllTrailing],
  metrics: nil,
  views: views)
allConstraints += summaryLabelVerticalConstraints

This code adds the NSLayoutFormatOptions options .alignAllLeading and .alignAllTrailing to the options array. The welcomeLabel and summaryLabel's leading and trailing spacing will be aligned 15 points from the leading and trailing edge of their superview. This occurs because the summaryLabel already has its horizontal constraints defined.

Again, this will give you the same layout as you already had, but in a better way.

Next, update the options when you create summaryToPageVerticalConstraints to match the following:

let summaryToPageVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[summaryLabel]-15-[pageControl(9)]-15-|",
  options: [.alignAllCenterX],
  metrics: nil,
  views: views)
allConstraints += summaryToPageVerticalConstraints

Adding this option aligns the views on the center X axis. Do the same for imageToWelcomeVerticalConstraints:

let imageToWelcomeVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[appImageView]-10-[welcomeLabel]",
  options: [.alignAllCenterX],
  metrics: nil,
  views: views)
allConstraints += imageToWelcomeVerticalConstraints

Build and run your project; how do the interface elements look?

Grapevine-SublayoutViewHeights

Feeling centered yet? Layout options have taken you closer to that nice user interface you're after.

NSLayoutFormat Options Quick Reference

Here are the options you've used in Grapevine:

  • .alignAllCenterX -- align interface elements using NSLayoutAttribute.centerX.
  • .alignAllCenterY -- align interface elements using NSLayoutAttribute.centerY.
  • .alignAllLeading -- align interface elements using NSLayoutAttribute.leading.
  • .alignAllTrailing -- align interface elements using NSLayoutAttribute.trailing.

Below are some more of these options:

  • .alignAllLeft -- align interface elements using NSLayoutAttribute.left.
  • .alignAllRight -- align interface elements using NSLayoutAttribute.right.
  • .alignAllTop -- align interface elements using NSLayoutAttribute.top.
  • .alignAllBottom -- align interface elements using NSLayoutAttribute.bottom.
  • .alignAllLastBaseline -- align interface elements using NSLayoutAttribute.lastBaseline.

You can find the complete list in the documentation.

Note: At least one of the elements must have enough defined perpendicular constraints for layout options to work. See the example below:

The topView, middleView or bottomView must have constraints defining the position of their leading edge for Auto Layout to generate the correct constraints.

  NSLayoutConstraints.constraintsWithVisualFormat(
    "V:[topView]-[middleView]-[bottomView]",
    options: [.alignAllLeading],
    metrics: nil,
    views: ["topView": topView, "middleView": middleView, "bottomView": bottomView"])
  NSLayoutConstraints.constraintsWithVisualFormat(
    "V:[topView]-[middleView]-[bottomView]",
    options: [.alignAllLeading],
    metrics: nil,
    views: ["topView": topView, "middleView": middleView, "bottomView": bottomView"])

And now for a new concept! Meet Metrics.

Metrics

Metrics are a dictionary of number values that can appear inside the VFL format string. These are particularly useful if you have standardized spacing or calculated size values that you can't type directly into the format string.

Add the following constant declaration above your @IBOutlet declarations in ViewController.swift:

private enum Metrics {
  static let padding: CGFloat = 15.0
  static let iconImageViewWidth: CGFloat = 30.0
}

Now you have a constant for the padding, and icon image width, you can create a metrics dictionary and utilize the constant. Add the following code above your views declaration in viewDidLoad():

let metrics = [
  "horizontalPadding": Metrics.padding,
  "iconImageViewWidth": Metrics.iconImageViewWidth]

The above code creates a dictionary of key / value pairs to be substituted into the format string.

Next, replace the topRowHorizontalConstraints and summaryHorizontalConstraints definitions with the following:

let topRowHorizontalFormat = """
  H:|-horizontalPadding-[iconImageView(iconImageViewWidth)]-[appNameLabel]-[skipButton]-horizontalPadding-|
  """
let topRowHorizontalConstraints = NSLayoutConstraint.constraints(
    withVisualFormat: topRowHorizontalFormat,
    options: [.alignAllCenterY],
    metrics: metrics,
    views: views)
allConstraints += topRowHorizontalConstraints

let summaryHorizontalConstraints = NSLayoutConstraint.constraints(
    withVisualFormat: "H:|-horizontalPadding-[summaryLabel]-horizontalPadding-|",
    metrics: metrics,
    views: views)
allConstraints += summaryHorizontalConstraints

You're replacing the hard coded values in the format string with placeholders that represent keys from the metrics dictionary. You also set the metrics parameter to the metrics dictionary.

Auto Layout will perform string substitution, replacing the placeholder text with the value inside the metrics dictionary. In the above case, horizontalPadding will be replaced with the constant 15 points, and iconImageViewWidth will be replaced with the constant 30 points.

You've removed a repeatedly used magic number and replaced it with a nice clean variable. If you decide to change the padding, you only have to change one thing. Isn't that better? The metrics dictionary isn't limited to constants either; if you need to calculate things at run time and put them in the dictionary, that's fine too.

The final piece of the puzzle to place is how you lay out interface elements when your view controllers are embedded inside a UINavigationController or UITabBarController.

Safe Area

The UI is starting to look great, but so far you've only tried it on the traditional, rectangle-shaped screens. In September 2017, Apple introduced a new device, which doesn't quite fit this description: the iPhone X. To see how Grapevine looks on this new device, start the app in the iPhone X simulator.

Well, it isn't the most pleasant sight. While the image, and the welcome text is mostly okay, you'll notice the UI interferes with system elements on both the top and the bottom of the screen. Luckily, with the use of safe area, you can easily work around this issue!

Introduced in iOS 11, safe area indicates the area in which apps can show their UI without interfering with any special elements defined by UIKit, like the status bar, or a tab bar. In case of the iPhone X, the safe area is different in portrait, and landscape mode:

You'll notice in portrait mode, there's more space on the top, and bottom. In landscape however, the left, and right insets are larger. So far you've put all your constraint-related code in viewDidLoad(), but since the safe area may change during runtime, that'll no longer cut it. Luckily, view controllers will be notified by the viewSafeAreaInsetsDidChange() on safe area changes, so you can start there.

Open, ViewController.swift and completely remove your viewDidLoad() method. That's right, you read correctly; you'll re-implement this functionality in viewSafeAreaInsetsDidChange() later.

Next, add the following property below your IBOutlet definitions:

private var allConstraints: [NSLayoutConstraint] = []

This property will store all currently active constraints within the view controller so they can be deactivated and removed when new constraints are required.

Next, add the following implementation for viewSafeAreaInsetsDidChange():

override func viewSafeAreaInsetsDidChange() {
  super.viewSafeAreaInsetsDidChange()

  if !allConstraints.isEmpty {
    NSLayoutConstraint.deactivate(allConstraints)
    allConstraints.removeAll()
  }

  let newInsets = view.safeAreaInsets
  let leftMargin = newInsets.left > 0 ? newInsets.left : Metrics.padding
  let rightMargin = newInsets.right > 0 ? newInsets.right : Metrics.padding
  let topMargin = newInsets.top > 0 ? newInsets.top : Metrics.padding
  let bottomMargin = newInsets.bottom > 0 ? newInsets.bottom : Metrics.padding
  
  let metrics = [
    "horizontalPadding": Metrics.padding,
    "iconImageViewWidth": Metrics.iconImageViewWidth,
    "topMargin": topMargin,
    "bottomMargin": bottomMargin,
    "leftMargin": leftMargin,
    "rightMargin": rightMargin]
}

The code above will make sure you remove any previously activated constraints otherwise you'll get auto layout errors. It also extends the metrics dictionary with calculated margins. You can access the new insets by the safeAreaInsets property of the view. In case of the rectangle-shaped phones, insets will be 0; the iPhone X however will have different values based on its orientation. To cater to both of these cases, you'll use the inset value if it's greater than zero, but will fall back to the padding defined earlier if it's not.

Finally, add the following constraints using the new metrics to the end of viewSafeAreaInsetsDidChange():

let views: [String: Any] = [
  "iconImageView": iconImageView,
  "appNameLabel": appNameLabel,
  "skipButton": skipButton,
  "appImageView": appImageView,
  "welcomeLabel": welcomeLabel,
  "summaryLabel": summaryLabel,
  "pageControl": pageControl]

let iconVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:|-topMargin-[iconImageView(30)]",
  metrics: metrics,
  views: views)
allConstraints += iconVerticalConstraints

let topRowHorizontalFormat = """
  H:|-leftMargin-[iconImageView(iconImageViewWidth)]-[appNameLabel]-[skipButton]-rightMargin-|
  """
let topRowHorizontalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: topRowHorizontalFormat,
  options: [.alignAllCenterY],
  metrics: metrics,
  views: views)
allConstraints += topRowHorizontalConstraints

let summaryHorizontalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "H:|-horizontalPadding-[summaryLabel]-horizontalPadding-|",
  metrics: metrics,
  views: views)
allConstraints += summaryHorizontalConstraints

let iconToImageVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[iconImageView]-10-[appImageView]",
  metrics: nil,
  views: views)
allConstraints += iconToImageVerticalConstraints

let imageToWelcomeVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[appImageView]-10-[welcomeLabel]",
  options: [.alignAllCenterX],
  metrics: nil,
  views: views)
allConstraints += imageToWelcomeVerticalConstraints

let summaryLabelVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[welcomeLabel]-4-[summaryLabel]",
  options: [.alignAllLeading, .alignAllTrailing],
  metrics: nil,
  views: views)
allConstraints += summaryLabelVerticalConstraints

let summaryToPageVerticalConstraints = NSLayoutConstraint.constraints(
  withVisualFormat: "V:[summaryLabel]-15-[pageControl(9)]-bottomMargin-|",
  options: [.alignAllCenterX],
  metrics: metrics,
  views: views)
allConstraints += summaryToPageVerticalConstraints

NSLayoutConstraint.activate(allConstraints)

Build and run the project, and you'll notice the updated UI looks much better on the iPhone X:

József Vesza

Contributors

József Vesza

Author

Jairo A. Cepeda

Tech Editor

Darren Ferguson

Final Pass Editor

Richard Critz

Team Lead

Over 300 content creators. Join our team.