Chapters

Hide chapters

Auto Layout by Tutorials

First Edition · iOS 13 · Swift 5.1 · Xcode 11

Section II: Intermediate Auto Layout

Section 2: 10 chapters
Show chapters Hide chapters

Section III: Advanced Auto Layout

Section 3: 6 chapters
Show chapters Hide chapters

4. Construct Auto Layout with Code
Written by Jayven Nhan

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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:

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.

import UIKit

final class ProfileViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
  }
}
let viewController = TabBarController()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController
window?.makeKeyAndVisible()
return true

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.

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

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.

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
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
view.leadingAnchor.constraint(equalTo: squareView.topAnchor)

Setting up profile header view

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

private let profileHeaderView = ProfileHeaderView()
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
}
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)])
view.backgroundColor = .white
setupProfileHeaderView()

Refactoring profile image view

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

// 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)
}
// 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
}

Refactoring profile name label

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

// 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)
}

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
  }
}
// 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
}()
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)
}
setupStackView()

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?

Symbols

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

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:

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.

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.

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.

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

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.

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

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.

[.alignAllLeading, .alignAllTrailing]

Setting up constraints

Open NewContactViewController.swift.

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

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.

Challenges

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

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.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now