跳转至

4 Construct Auto Layout with Code

There are two ways to implement Auto Layout into your projects. You already learned how to implement Auto Layout using Interface Builder. Now, it’s time to learn the second approach: using code.

Almost every iOS developer eventually raises the question: Should you construct your UI using storyboards or code? There is no silver bullet, and no one-size-fits-all solutions; however, there are solutions that fit much better based on your specific needs and requirements.

To help you achieve fluency in constructing UIs using code, you’ll learn about the following topics in this chapter:

  • Launching a storyboard view controller using code.
  • Launching a non-storyboard view controller using code.
  • Refactoring Interface Builder UIs into code.
  • Using visual format language to construct Auto Layout.
  • Benefits and drawbacks of constructing Auto Layout using code.

As a developer, you’ll see projects implement their UIs using Interface Builder, code, and in some cases, both approaches within the same project. To build optimized solutions for new projects, and to help maintain existing projects, it’s vitally important to understand both methods of building an app’s UI.

By the end of this chapter, you’ll know how to use code interchangeably with Interface Builder. You’ll also gain the knowledge to make more decisive presentation logic decisions to achieve more optimal solutions.

Launching a view controller from the storyboard in code

Open MessagingApp.xcodeproj in the starter folder, and then open the project target’s general settings. Set the Main Interface text field to empty.

img

Build and run, and you’ll see a black screen.

With the Interface Builder implementation, the app launches the initial view controller of the storyboard set in the target’s Main Interface. To do something similar in code, you need to take a different approach.

Open AppDelegate.swift and replace the code inside application(_:didFinishLaunchingWithOptions:) with the following:

// 1
let storyboard = UIStoryboard(name: "TabBar", bundle: nil)
// 2
let viewController =
  storyboard.instantiateInitialViewController()
// 3
window = UIWindow(frame: UIScreen.main.bounds)
// 4
window?.rootViewController = viewController
// 5
window?.makeKeyAndVisible()
return true

Here’s what you’ve done:

  1. Initialize the storyboard in code using the storyboard name.
  2. Create a reference to the storyboard’s initial view controller.
  3. Set the app delegate’s window using the device’s screen size as the frame.
  4. Set the window’s root view controller to the storyboard’s initial view controller.
  5. By calling makeKeyAndVisible() on your window, window is shown and positioned in front of every window in your app. For the most part, you’ll only need to work with one window. There are instances where you’d want to create new windows to display your app’s content. For example, you’ll work with multiple windows when you want to support an external display in your app. Chapter 17, “Auto Layout for External Displays”, covers supporting external displays.

When you use storyboards, the app delegate’s window property is automatically configured. In contrast, when you use code, you need to do more manual work. This is generally true when using code over Interface Builder.

Build and run, and you’ll see the following:

img

That’s only a taste of what it’s like using more code and less Interface Builder. Are you ready for some more?

Launching a view controller without initializing storyboard

You now know how to launch a view controller in code from a storyboard. But no set rule dictates that a project can’t mix storyboards/.xibs and code. For example, there may come a time where your team’s objective is to refactor an existing codebase that uses storyboards/.xibs into one that uses code. You’re going to do that now.

First, delete the following Interface Builder files:

  • Profile.storyboard
  • TabBar.storyboard

Then, remove all of the code inside ProfileViewController’s body except for viewDidLoad().

When you’re done, ProfileViewController.swift will look like this:

import UIKit

final class ProfileViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }
}

Up next, you’ll create your UI properties and layout in code rather than using Interface Builder.

Open AppDelegate.swift.

Note

Press Command-Shift-O to open quickly. Type AppDelegate. Xcode will suggest AppDelegate.swift. Press Return to open the file.

Replace the existing code inside application(_:didFinishLaunchingWithOptions:)with the following:

let viewController = TabBarController()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController
window?.makeKeyAndVisible()
return true

Here, you command the app to initialize TabBarController using its initializer method.

Build and run, and you’ll see a tab bar controller with a black background.

img

When you add a view controller onto a storyboard, the background view is set to white by default. In code, the view controller’s view has a nil background color, which shows up as black. Once again, this is one of the automated steps as a result of using Interface Builder.

One of the benefits of initializing your view controller with its initializer method is that the view controller’s type is explicit. Whereas when you initialize a view controller from a storyboard’s initial view controller, you need additional code to ensure that the view controller returned is TabBarController.

Next, you’ll rebuild the profile view controller user interface in code.

Building out a view controller’s user interface in code

When you built the UI in Interface Builder, the profile view controller had a header view with a gray background. The header view encapsulated the main stack view, and the main stack view encapsulated two stack views. One stack view contained the profile image view and the full name label. The other stack view contained the action buttons. It’s time to recreate those user interfaces in code.

Create a new Swift file named ProfileHeaderView.swift inside the User Profile/Viewsgroup.

Replace the existing template code with the following:

import UIKit

final class ProfileHeaderView: UIView {
  override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = .groupTableViewBackground
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
}

With the code above, you override init(frame:). The method you’ve overridden is an initializer method. As the method name implies, this is where you add your initialization code, such as setting the view’s background color.

Afterward, init(coder:) takes care of the object’s archiving and unarchiving processes for Interface Builder. This method is handy when you launch an object from storyboard/.xib and want to configure the object at the initialization phase. Generally, when a view is created in code, init(frame:) is the initializer used, and when a view is created from a storyboard or .xib, init(coder:) is used instead.

Layout anchors

Before the introduction of layout anchors, developers created constraints in code using NSLayoutConstraint initializers. NSLayoutConstraint describes the relationship between two user interface objects, and that relationship has to satisfy the Auto Layout engine. Although this approach still works, there’s room for improvements in the code readability and cleanliness departments. Consequently, Apple introduced layout anchors for this purpose.

NSLayoutAnchor is a generic class built to create layout constraints using a fluent interface.

Are you ready for a comparison between constraints created using NSLayoutConstraintinitializers and constraints created using layout anchors? Sure you are!

Say you’d like to create a square view centered vertically and horizontally inside of a view controller’s view. To create this layout in code using NSLayoutConstraint initializers, it would look like this:

NSLayoutConstraint(
  item: squareView,
  attribute: .centerX,
  relatedBy: .equal,
  toItem: view,
  attribute: .centerX,
  multiplier: 1,
  constant: 0).isActive = true

NSLayoutConstraint(
  item: squareView,
  attribute: .centerY,
  relatedBy: .equal,
  toItem: view,
  attribute: .centerY,
  multiplier: 1,
  constant: 0).isActive = true

NSLayoutConstraint(
  item: squareView,
  attribute: .width,
  relatedBy: .equal,
  toItem: nil,
  attribute: .notAnAttribute,
  multiplier: 0,
  constant: 100).isActive = true

NSLayoutConstraint(
  item: squareView,
  attribute: .width,
  relatedBy: .equal,
  toItem: squareView,
  attribute: .height,
  multiplier: 1,
  constant: 0).isActive = true

Whereas, creating constraints in code using layout anchors would look like this:

squareView.centerXAnchor.constraint(
  equalTo: view.centerXAnchor).isActive = true
squareView.centerYAnchor.constraint(
  equalTo: view.centerYAnchor).isActive = true
squareView.widthAnchor.constraint(
  equalToConstant: 100).isActive = true
squareView.widthAnchor.constraint(
  equalTo: squareView.heightAnchor).isActive = true

Between the two approaches, using layout anchors is comparatively cleaner, more succinct and more readable. The benefits of using layout constraints don’t just stop at the fluent interface.

Another benefit you get for using layout anchors is type checking. Type checking mitigates the chance of creating invalid constraints in code. Type checking helps validate horizontal axis, vertical axis, height and width constraints.

If you try to compile the following code:

view.leadingAnchor.constraint(equalTo: squareView.topAnchor)

The compiler won’t allow it because this code tries to create a constraint between an NSLayoutXAxisAnchor and an NSLayoutYAxisAnchor. The compiler recognizes that the constraint is invalid, and invalid constraints are reported as build-time errors thanks to layout anchor’s type checking.

Using layout anchors won’t prevent you from creating invalid constraints entirely. Despite the type checking efforts, layout anchors are still susceptible to invalid constraints. This is because the compiler allows you to create constraints between one view’s leading and trailing layout anchor and another view’s left/right layout anchor. This constraint is allowed because both anchors are x-axis layout anchors. Therefore, the constraint compiles fine in Xcode.

As it turns out, however, the Auto Layout engine restricts the relationship between trailing and leading layout anchors with left or right layout anchors. Such constraints will cause a runtime crash.

Now that you understand the essence of layout anchors and their benefits, it’s a great time to put them to work in your project.

Setting up profile header view

Open ProfileViewController.swift and add the following property to ProfileViewController:

private let profileHeaderView = ProfileHeaderView()

Next, add the following method to ProfileViewController:

private func setupProfileHeaderView() {
  // 1
  view.addSubview(profileHeaderView)
  // 2
  profileHeaderView.translatesAutoresizingMaskIntoConstraints =
    false
  // 3
  profileHeaderView.leadingAnchor.constraint(
    equalTo: view.leadingAnchor).isActive = true
  profileHeaderView.trailingAnchor.constraint(
    equalTo: view.trailingAnchor).isActive = true
  profileHeaderView.topAnchor.constraint(
    equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
  profileHeaderView.bottomAnchor.constraint(
    lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor)
    .isActive = true
}

With this code, you:

  1. Add profileHeaderView as a subview of the view controller’s view.
  2. Set profileHeaderView.translatesAutoresizingMaskIntoConstraints to false. Before Auto Layout behaves as you’d expect from Interface Builder, this is a property that you need to remember to always set to false. It’s set to trueby default. Autoresizing mask is Auto Layout’s predecessor. It’s a layout system that’s a lot less comprehensive when compared to Auto Layout.
  3. Set and activate the profile header view’s leading, trailing, top and bottom anchors.

You can now refactor the code you added earlier.

Replace the code in setupProfileHeaderView() with this:

view.addSubview(profileHeaderView)
profileHeaderView.translatesAutoresizingMaskIntoConstraints =
  false
NSLayoutConstraint.activate(
  [profileHeaderView.leadingAnchor.constraint(
    equalTo: view.leadingAnchor),
   profileHeaderView.trailingAnchor.constraint(
    equalTo: view.trailingAnchor),
   profileHeaderView.topAnchor.constraint(
    equalTo: view.safeAreaLayoutGuide.topAnchor),
   profileHeaderView.bottomAnchor.constraint(
    lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor)])

This code looks cleaner and is relatively more performant because of the Auto Layout engine’s life cycle. Instead of bringing up the Auto Layout engine three additional times, you bring on the Auto Layout engine once and activate a list of constraints at one time versus individually. Although Interface Builder internal configurations aren’t completely explicit, it’s likely that this is also an automatic optimization implemented for developers who work with Interface Builder.

Now, add the following code to the end of viewDidLoad():

view.backgroundColor = .white
setupProfileHeaderView()

This code sets the view’s background color to white. It then calls the method you implemented to set up the profile header view.

Build and run, and you’ll see the following screen:

img

Refactoring profile image view

It’s time to create the profile image view in code.

Open ProfileImageView.swift and remove @IBDesignable from the class declaration.

After that, remove everything inside ProfileImageView, and replace the body of ProfileImageView with the following:

// 1
enum BorderShape: String {
  case circle
  case squircle
  case none
}

let boldBorder: Bool

var hasBorder: Bool = false {
  didSet {
    guard hasBorder else { return layer.borderWidth = 0 }
    layer.borderWidth = boldBorder ? 10 : 2
  }
}

// 2
private let borderShape: BorderShape

// 3
init(borderShape: BorderShape, boldBorder: Bool = true) {
  self.borderShape = borderShape
  self.boldBorder = boldBorder
  super.init(frame: CGRect.zero)
  backgroundColor = .lightGray
}

// 4
convenience init() {
  self.init(borderShape: .none)
}

// 5
required init?(coder aDecoder: NSCoder) {
  self.borderShape = .none
  self.boldBorder = false
  super.init(coder: aDecoder)
}

Here’s how this code works:

  1. First, you declare a BorderShape enum. This contains an additional none case for when you want the image view to have no particular border shape. Notice that the access modifier for BorderShape is no longer private. Instead, it’s internal since there is no other explicit access modifier declared on the property. This is so that BorderShape becomes accessible to other objects when they initialize ProfileImageView. boldBorder is used to determine ProfileImageView’s layer border width for when hasBorder is true. When hasBorder is false, the view’s layer border width is simply set to zero.
  2. You also have borderShape, which has changed from a string type to use the enum. One of the great benefits of building your UI in code is that you can make use of a lot more great Swift features. With Interface Builder, configuring a view’s property using an enum isn’t really an option.
  3. Then, there’s init(borderShape:boldBorder:). This method initializes ProfileImageView by taking in the borderShape and boldBorder parameters and setting the class properties appropriately. Then, the method calls the superclass initializer method and passes in .zero for the frame. The frame size isn’t a concern since the ProfileImageView will use Auto Layout to determine the view’s size and position. Next, you set the background color to light gray.
  4. Here, you have a convenience initializer which mitigates the need to pass in a BorderShape into the first initializer. This convenience initializer allows you initialize a ProfileImageView as ProfileImageView(). When you use this convenience initializer, borderShape is set to none.
  5. Finally, you have init(coder:), which is used when you create ProfileImageView in Interface Builder. In this case, you initialize borderShapeto none and boldBorder to false.

You’ve added the value type, properties and initializer methods. It’s time to configure the border.

Add the following code to ProfileImageView:

// 1
override func layoutSubviews() {
  super.layoutSubviews()
  setupBorderShape()
}

private func setupBorderShape() {
  hasBorder = borderShape != .none
  // 2
  let width = bounds.size.width
  let divisor: CGFloat
  switch borderShape {
  case .circle:
    divisor = 2
  case .squircle:
    divisor = 4
  case .none:
    divisor = width
  }
  let cornerRadius = width / divisor
  layer.cornerRadius = cornerRadius
}

Here’s what you added:

  1. layoutSubviews() is called when the constraint-based layout has finished its configuration. This is the time when ProfileImageView sets up the border shape of the view by calling setupBorderShape().
  2. Inside of setupBorderShape(), borderShape determines the corner radius. When borderShape is a circle, the layer’s corner radius will be half of the view’s width. When borderShape is a squircle, the layer’s corner radius will be a quarter of the view’s width. When borderShape is set to none, the layer’s corner radius will be 1.

You’re well on your way to becoming an Auto Layout code warrior. But there’s still more to do.

Refactoring profile name label

Open ProfileNameLabel.swift and remove @IBDesignable from the class declaration.

Next, replace everything inside of ProfileNameLabel with the following code:

// 1
override var text: String? {
  didSet {
    guard let words = text?
      .components(separatedBy: .whitespaces) 
      else { return }
    let joinedWords = words.joined(separator: "\n")
    guard text != joinedWords else { return }
    DispatchQueue.main.async { [weak self] in
      self?.text = joinedWords
    }
  }
}

// 2
init(fullName: String? = "Full Name") {
  super.init(frame: .zero)
  setTextAttributes()
  text = fullName
}

// 3
required init?(coder: NSCoder) {
  super.init(coder: coder)
}

// 4
private func setTextAttributes() {
  numberOfLines = 0
  textAlignment = .center
  font = UIFont.boldSystemFont(ofSize: 24)
}

With this code, you:

  1. Override text to add a didSet observer. Every time the text value changes, didSet will get called. Safely unwrap using the guard statement to make sure that there’s at least one word to extract from text. If text is nil, simply return and do nothing. If text is not nil, then add \n between every word. The \n separator creates a line spacing between each word. Afterward, ProfileImageView’s text is set to the new string joined by a separator that you create.
  2. The initializer method takes parameters to set its properties and calls init(frame:) on the super class. Next, you call setTextAttributes() to set up some UILabel properties.
  3. Implement the required initializer for when Interface Builder initializes ProfileNameLabel.
  4. Finally, setTextAttributes sets the numberOfLines, textAlignment and font properties.

So, that’s how you create the profile name label in code. You’re ready to look at refactoring the stack views from Interface Builder.

Refactoring stack views

To begin rebuilding the stack views in code, add the following extension to the bottom of ProfileHeaderView.swift (outside of the class):

private extension UIButton {
  static func createSystemButton(withTitle title: String)
    -> UIButton {
      let button = UIButton(type: .system)
      button.setTitle(title, for: .normal)
      return button
  }
}

This is a convenience method you can use to create a system button with the given title.

Next, add the following properties to ProfileHeaderView:

// 1
private let profileImageView =
  ProfileImageView(borderShape: .squircle)
private let leftSpacerView = UIView()
private let rightSpacerView = UIView()

private let fullNameLabel = ProfileNameLabel()

// 2
private let messageButton =
  UIButton.createSystemButton(withTitle: "Message")
private let callButton =
  UIButton.createSystemButton(withTitle: "Call")
private let emailButton =
  UIButton.createSystemButton(withTitle: "Email")

// 3
private lazy var profileImageStackView =
  UIStackView(arrangedSubviews: 
    [leftSpacerView, profileImageView, rightSpacerView])

private lazy var profileStackView: UIStackView = {
  let stackView = UIStackView(arrangedSubviews:
    [profileImageStackView, fullNameLabel])
  stackView.distribution = .fill
  stackView.axis = .vertical
  stackView.spacing = 16
  return stackView
}()

private lazy var actionStackView: UIStackView = {
  let stackView = UIStackView(arrangedSubviews:
    [messageButton, callButton, emailButton])
  stackView.distribution = .fillEqually
  return stackView
}()

private lazy var stackView: UIStackView = {
  let stackView = UIStackView(arrangedSubviews:
    [profileStackView, actionStackView])
  stackView.axis = .vertical
  stackView.spacing = 16
  return stackView
}()

With this code, you:

  1. Initialize the profile image view, left spacer view, right spacer view and full name label.
  2. Initialize the three action buttons using createSystemButton(withTitle:), which you added earlier.
  3. Initialize four stack views. The first stack view contains the profile image view and the spacer views. The second stack view encapsulates the profile image stack view and the full name label. The third stack view includes the action buttons. The fourth stack view stacks the second and third stack views together.

You’re ready to set up the layout of the stack view. Add the following method to ProfileHeaderView:

private func setupStackView() {
  // 1
  addSubview(stackView)
  stackView.translatesAutoresizingMaskIntoConstraints = false

  // 2
  NSLayoutConstraint.activate(
    [stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
     stackView.leadingAnchor.constraint(
      greaterThanOrEqualTo: leadingAnchor, constant: 20),
     stackView.leadingAnchor.constraint(
      lessThanOrEqualTo: leadingAnchor, constant: 500),
     stackView.bottomAnchor.constraint(
      equalTo: bottomAnchor, constant: -8),
     stackView.topAnchor.constraint(
      equalTo: topAnchor, constant: 26),

     profileImageView.widthAnchor.constraint(
      equalToConstant: 120),
     profileImageView.widthAnchor.constraint(
      equalTo: profileImageView.heightAnchor),

     leftSpacerView.widthAnchor.constraint(
      equalTo: rightSpacerView.widthAnchor)
    ])

  // 3
  profileImageView.setContentHuggingPriority(
    UILayoutPriority(251), 
    for: NSLayoutConstraint.Axis.horizontal)
  profileImageView.setContentHuggingPriority(
    UILayoutPriority(251),
    for: NSLayoutConstraint.Axis.vertical)

  fullNameLabel.setContentHuggingPriority(
    UILayoutPriority(251),
    for: NSLayoutConstraint.Axis.horizontal)
  fullNameLabel.setContentHuggingPriority(
    UILayoutPriority(251),
    for: NSLayoutConstraint.Axis.vertical)
  fullNameLabel.setContentCompressionResistancePriority(
    UILayoutPriority(751),
    for: NSLayoutConstraint.Axis.vertical)

  messageButton.setContentCompressionResistancePriority(
    UILayoutPriority(751),
    for: NSLayoutConstraint.Axis.horizontal)
}

Here, you:

  1. Add stackView as the view’s subview. Then, set translatesAutoresizingMaskIntoConstraints to false so that your Auto Layout constraints can behave correctly without the interference of the autoresizing masks.
  2. Set up the Auto Layout constraints on stackView and its subviews to match the layout behavior from the Interface Builder implementation.
  3. Set the content hugging priority and compression resistance priority on profileImageView, fullNameLabel and messageButton to match the layout behavior from the Interface Builder implementation.

Finally, add the following code to init(frame:):

setupStackView()

And voilà! You have yourself a ProfileViewController created entirely in code.

Build and run, and you’ll see something like this:

img

There’s more to learn about creating constraints in code.

Auto Layout with visual format language

Using visual format language is another way of constructing your Auto Layout in code. Building Auto layout in code has come a long way in terms of code readability since the debut of visual format language. As you may have guessed, visual format language isn’t exactly the most user-friendly tool available. So you may be wondering: Why should anyone learn visual format language to construct Auto Layout then?

Here are two reasons to get familiar with it:

  1. Refactor or maintain legacy code which constructs Auto Layout constraints using visual format language.
  2. Read and comprehend Auto Layout runtime errors.

The second reason is particularly significant for anyone who works with Auto Layout using Interface Builder or code. Because you’ll see a lot of symbols with visual format language, it’s a good idea to get familiar with them.

Symbols

For reference, here are the symbols to describe your layout in visual format language:

  • | superview
  • - standard spacing (usually 8 points; value can be changed if it is the spacing to the edge of a superview)
  • == equal widths
  • -20- non-standard spacing (20 points)
  • <= less than or equal to
  • >= greater than or equal to
  • @250 priority of the constraint; can have any value between 0 and 1000
  • 250 - low priority
  • 750 - high priority
  • 1000 - required priority

Visual format string example

H:|-[label(labelHeight)]-16-[imageView(>=250,<=300)]-16-[button(88@250)]-|

Here’s what the string above does to create constraints:

  • H: indicates that the constraints are for the horizontal arrangement.
  • |-[label creates a constraint between the superview’s leading edge and the label’s leading edge. label should be found in the views dictionary. More on views dictionary in the following sections.
  • label(labelHeight) sets the label’s height to labelHeight. This key should be found in the metrics dictionary. More on the metrics dictionary in the following sections.
  • ]-16-[imageView sets a constraint with 16 spacings between label’s trailing edge and imageView’s leading edge.
  • [imageView(>=250,<=300)] sets a width greater than or equal to 250 constraint and less than or equal to 300 constraint on imageView.
  • ]-16-[button sets a 16 spacings constraint between imageView’s trailing edge and button’s leading edge.
  • [button(88@250)] gives button a width constraint equal to 88 with a low priority. This allows the Auto Layout engine to break this constraint when needed.
  • ]-| sets a constraint with standard spacing between button’s trailing edge and the superview’s trailing edge.

Thinking visual format language

Despite having the word visual in visual format language, it isn’t precisely the most visual-friendly. It’s important to have the right strategy to think about constraints to effectively create or maintain constraints created using visual format language.

From reading visual format language string inputs, although not visual-friendly code, it can express a lot to the Auto Layout engine. In this section, you’ll learn to express visual format language string inputs.

Horizontal and vertical axes

When you think about visual format language, imagine either drawing a line from top-to-bottom or left-to-right on your device. Then, looking at the line you drew, track down the property name of every UI object the line passes through.

Look at the following diagram:

img

Count the number of red lines on the diagram. There are five. In your constraints code, you’ll create five sets of constraints using visual format language: three horizontal arrangements and two vertical arrangements. Notice the connection? Five lines, five sets of constraints. This is generally true unless you prefer to split the constraints up or utilize the layout options parameter for certain scenarios when creating your constraints.

You use either an H: or V: to specify horizontal or vertical arrangements, respectively, in a visual format string.

Great, you know the sets of constraints to create when you see a layout. Now, you’ll learn to inform the constraints about view position and spacing, and you’ll learn how metrics and views in a visual format string come together to create constraints.

Metrics dictionary

You can define metrics string with a dictionary using visual format language. You can then use a metrics key-value pair to define a constraint’s constant or multiplier. You may want to use the metrics dictionary to pass in values for the visual format string to reference.

A metrics dictionary can look like this:

["topSpacing": topSpace,
 "textFieldsSpacing": 8]

The dictionary you just saw consists of a key-value pair with a topSpacing key. The value corresponding to the key is a constant declared as topSpace. There’s another key-value pair with a textFieldsSpacingkey. It has a value of 8.

When the visual format string makes use of topSpacing or textFieldsSpacing, the value is referenced from the metrics dictionary.

Views dictionary

So, how are you going to tell the constraints about the views using visual format language? This is where the views dictionary comes into play. Similar to the metrics dictionary, your key-value pairs consist of a string and the view object for the key and value, respectively.

For example, look at the following dictionary:

["textField": textField,
 "imageView": imageView]

Your visual format string can use the textField string to reference the textFielduser interface object. You can also use the imageViewstring to reference the imageViewuser interface object.

The metrics and views dictionaries will make more sense when you build out your user interface using visual language format.

Layout options

When creating your constraints using visual format language, you’ll have the option to use the options parameter. options lets you describe the views’ constraints perpendicular to the current layout orientation.

Look at the following diagram:

img

Imagine the top view has its leading, top, trailing and height constraints. The bottom view has its top and height constraints. The bottom view is missing the horizontal constraints arrangement. This is where layout options are handy.

You can use the following layout options:

[.alignAllLeading, .alignAllTrailing]

This helps you achieve the diagram’s bottom view layout. Also, this tells the Auto Layout engine that you want all of your views in a visual format string to share the same leading and trailing constraints. The top view has its leading and trailing constraints. Thus, the bottom view simply infers these constraints and shares the top view’s leading and trailing constraints.

That’s enough theory for now; you’re ready to get your hands dirty with visual format language code implementation.

Setting up constraints

Open NewContactViewController.swift.

You can see that there’s some existing code within NewContactViewController. The focus of this section is on the Auto Layout constraint construction for profileImageView, firstNameTextField and lastNameTextField using visual format language. You’ll do this in setupViewLayout(), which is called from viewSafeAreaInsetsDidChange().

When the root view’s safe area insets change, iOS calls viewSafeAreaInsetsDidChange() to inform your app of the latest safe area insets. Upon calling the method, constraints are deactivated and emptied in preparation for handing the newest constraints.

Add the following code to setupViewLayout():

// 1
let safeAreaInsets = view.safeAreaInsets

let marginSpacing: CGFloat = 16
let topSpace = safeAreaInsets.top + marginSpacing
let leadingSpace = safeAreaInsets.left + marginSpacing
let trailingSpace = safeAreaInsets.right + marginSpacing

// 2
var constraints: [NSLayoutConstraint] = []

// 3
view.addSubview(profileImageView)
profileImageView.translatesAutoresizingMaskIntoConstraints =
  false
view.addSubview(firstNameTextField)
firstNameTextField.translatesAutoresizingMaskIntoConstraints =
  false
view.addSubview(lastNameTextField)
lastNameTextField.translatesAutoresizingMaskIntoConstraints =
  false

With this code, you:

  1. Define top, leading and trailing margin spacing constants that are used to create Auto Layout constraints.
  2. Initialize an empty array of type NSLayoutConstraint collection.
  3. Add the profile image view and text fields to the view hierarchy. You also disable the autoresizing mask, which you need to do for views created in code that use Auto Layout.

Add the following code to the end of setupViewLayout():

// 1
let profileImageViewVerticalConstraints =
  NSLayoutConstraint.constraints(withVisualFormat:
    "V:|-topSpacing-[profileImageView(profileImageViewHeight)]",
    options: [],
    metrics:
    ["topSpacing": topSpace, "profileImageViewHeight": 40],
    views: ["profileImageView": profileImageView])
constraints += profileImageViewVerticalConstraints

// 2
let textFieldsVerticalConstraints =
  NSLayoutConstraint.constraints(withVisualFormat:
    "V:|-topSpacing-[firstNameTextField(profileImageView)]-textFieldsSpacing-[lastNameTextField(firstNameTextField)]",
    options: [.alignAllCenterX],
    metrics: [
      "topSpacing": topSpace,
      "textFieldsSpacing": 8],
    views: [
      "firstNameTextField": firstNameTextField,
      "lastNameTextField": lastNameTextField,
      "profileImageView": profileImageView])
constraints += textFieldsVerticalConstraints

// 3
let profileImageViewToFirstNameTextFieldHorizontalConstraints =
  NSLayoutConstraint.constraints(withVisualFormat:
    "H:|-leadingSpace-[profileImageView(profileImageViewWidth)]-[firstNameTextField(>=200@1000)]-trailingSpace-|",
    options: [],
    metrics: [
      "leadingSpace": leadingSpace,
      "trailingSpace": trailingSpace,
      "profileImageViewWidth": 40],
    views: [
      "profileImageView": profileImageView,
      "firstNameTextField": firstNameTextField])
constraints += 
  profileImageViewToFirstNameTextFieldHorizontalConstraints

// 4
let lastNameTextFieldHorizontalConstraints =
  NSLayoutConstraint.constraints(
    withVisualFormat: 
      "H:[lastNameTextField(firstNameTextField)]",
    options: [],
    metrics: nil,
    views: [
      "firstNameTextField": firstNameTextField,
      "lastNameTextField": lastNameTextField])
constraints += lastNameTextFieldHorizontalConstraints

// 5
NSLayoutConstraint.activate(constraints)
self.constraints = constraints

In this example, the constraints look something like this on a diagram:

img

So, with this code, you:

  1. You create a constraint with topSpacing spacings between the superview’s top edge and profileImageView’s top edge. profileImageView’s height constraint is set to equal to profileImageViewHeight. Finally, you add the vertical constraints you created into the constraints array.
  2. You create a constraint with topSpacing spacings between the superview’s top edge and firstNameTextField’s top edge. firstNameTextField’s height is set to equal to profileImageView. Next, you create a constraint with textFieldsSpacing spacings between firstNameTextField’s bottom edge and lastNameTextField’s top edge. lastNameTextField’s height constraint is set to equal to firstNameTextField. Finally, you add the vertical constraints you created into the constraints array.
  3. You create a constraint with leadingSpace spacings between the superview’s leading edge and profileImageView’s leading edge. The profileImageView’s width constraint is set to equal to profileImageViewWidth. You add a constraint with standard spacing between profileImageView’s trailing edge and firstNameTextField’s leading edge. The firstNameTextField’s width constraint is set with a required constraint priority and a minimum width value of 200. Before wrapping up the visual format string, you set a spacing equal to trailingSpace between firstNameTextField’s trailing edge and the superview’s trailing edge. Finally, you add the horizontal constraint you created into the constraints array.
  4. The next set of constraints comes from the second horizontal line coming down from the diagram. You set up lastNameTextField’s width equal to firstNameTextField. It’s unnecessary to set additional spacing thanks to the centering of the x-axis from step #2. Finally, you add the horizontal constraint you created into the constraints array
  5. Activate all of the constraints inside of constraints. Then, set the recently created constraints to NewContactViewController’s constraints. This is so you can deactivate the constraints for when the safe area insets change. For example, when the device’s orientation changes.

Build and run. Tap the Contacts tab. Tap the + tab bar button. You’ll see:

img

There you have it: Your layout built in code using visual language format. However, it’s usually more work for the same design. Plus, visual language format is not the most user-friendly. Having said that, understanding visual format language can still assist you in debugging constraint problems, maintaining legacy codebases, refactoring legacy codebases and more.

Benefits and drawbacks from choosing the code approach

Whether you choose Interface Builder or code to layout your user interface, it is an unquestionably subjective matter. Before you come to a decisive conclusion on your approach, have a look at the benefits and drawbacks when using code to construct Auto Layout.

Here are five benefits of using code to construct Auto Layout constraints:

  • Everything technical that storyboard can do, code can do too. But, not vice versa.
  • Readable merge conflict(s) means less time spent fixing merge conflicts.
  • Code compilation time reduction.
  • All the user interface logic lives in code. This benefit mitigates events such as having to find whether a property is changed in Interface Builder or code.
  • Easy UI maintenance with the right coding infrastructure. Manage UI constants such as fonts, colors and constraint values with ease.

Here are five drawbacks of using code:

  • Higher learning curve compared to using Interface Builder.
  • Naming conventions and code cleanliness are vital for code maintenance when working on a team.
  • Inability to add user interface objects and create constraints for the user interface objects visually like in Interface Builder.
  • More developers are familiar with using Interface Builder than developers who are familiar with using code to build an app’s layout. If you work with developers who are less familiar with using code, more time may be needed to properly onboard the developers.
  • Opportunity cost of missing out some of the automation Interface Builder provides when you build your layouts in Interface Builder.

Because there’s no one-size-fits-all solution, one of the more important steps to coming to the optimal solution is clearly defining the problem. What exactly are you or your team trying to solve?

There are benefits and drawbacks to using Interface Builder and code. You need to consider the tradeoffs before deciding which to use. Questions such as:

  • Does it make sense for a project to sacrifice Interface Builder automation for ease of source control when merge conflicts arise?
  • Is it preferable to keep Interface Builder visualizations in the sacrifice of reduced compile time?

Personalization is a big consideration. For example:

  • Which of the tradeoffs apply to the scenario at hand?
  • What about personal preferences? Everyone has their personal preferences. Some people enjoy using Interface Builder over code, and others do not.

When choosing an approach to build your app’s UI, take time to consider the problems, tradeoffs and personalizations fully. The optimal solution will arise by building a solution tailored to your specific team and project requirements. Ultimately, it’s up to you and your team to decide.

Challenges

You’ve reached the end of the chapter. To help solidify your understanding, try these challenges:

  • Recreate the ContactListTableViewController’s UI entirely in code.
  • Recreate the ContactTableViewCell’s UI entirely in code.
  • Recreate the ContactPreviewView’s UI entirely in code.

Key points

  • Working with code requires more upfront manual work than working with Interface Builder.
  • You can refactor UI layouts built in Interface Builder into code format.
  • There are various methods to create Auto Layout constraints using code.
  • Learning visual format language, although rarely seen on new projects, can assist you in debugging constraint conflicts and maintaining legacy codebases.
  • Consider the pros and cons when choosing between Interface Builder and code approach to creating your UI layout.