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

10. Adaptive Layout
Written by Libranner Santos

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Adaptability is all about guaranteeing a good user experience across all iOS devices and screen sizes. With so many options, it can be challenging to develop apps that look good on everything. Unfortunately, creating storyboards and views for each screen size and orientation doesn’t scale well, so it’s critical to build your apps with adaptive user interfaces that use adaptive layouts.

Adaptive apps rely on the trait system and trait collections. A trait collection is a set of traits and their respective values. A trait describes the current environment for your app. For example, traits can include layout direction, dynamic type size and size classes.

The main goal of adaptive layout is to allow you to create apps for all iOS devices without the need for device-specific code. In this chapter, you’ll learn how to create adaptable apps by using size classes and adaptive images. Throughout this chapter, you’ll use the tools that UIKit already provides. For more adaptive layout content, read Chapter 15, “Dynamic Type,” and Chapter 16, “Internationalization and Localization.”

One storyboard to run them all

Depending on the complexity of your app, you can use different strategies to accomplish adaptability. By using the right constraints, your screens can adapt gracefully to different screen sizes and orientations.

Go to the starter project and open the MessagingApp project. Select the iPhone 8 simulator, then build and run.

The Profile screen looks good in portrait mode. Now, press Command-Right Arrow to switch the simulator orientation to landscape.

Notice the views aren’t using all of the available width. Go back to Xcode, and open Profile.storyboard. In the document outline, look for Profile Scene and select Main Stack View. Open the Size inspector.

Look at the constraints; there’s one for the width to make sure it’s equal to 375. Select that constraint and press delete to remove it. Since the available screen size isn’t always going to be 375, it doesn’t make sense to keep this constraint.

Select Main Stack View in the document outline, and Control-drag to View, which is located at the top of the document outline. On the modal window that pops up, select Equal Widths.

Now, build and run.

Rotate the device from portrait to landscape and then back to portrait. Notice how the screen adapts to the available width after deleting the width constraint.

Setting up the storyboard

Go to Main.storyboard, and press Command-Option-1 to show the File inspector. On the Interface Builder Document section, make sure that Use Trait Variations and Use Safe Area Layout Guides are both checked. Note that these options are selected by default in the latest versions of Xcode.

Previewing layouts

On the bottom bar in Interface Builder, click View as: <iOS Device>. This action expands the Device Configuration Bar.

Size classes

When creating layouts, you should always think in terms of available space. Size classes make it possible to know how much available space there is by taking into account the device and the environment in which the app is running. For example, apps running on an iPhone 6 won’t have the same available space as apps running on an iPad.

Multitasking and size classes

As you can see in the previous table, for the iPad, you usually have regular width and height. This changes when the system is using a split view since there’s less space available.

Working with size classes

Go back to Xcode. Open Main.storyboard and select the About Scene. The view contains two elements: an image view and a label — neither have constraints. Build and run to see the About Scene.

Changing properties

Apart from constraints, you can also change the value of some properties using size classes.

Trait environment and trait collections

Every time a device changes its orientation, traits containing the new configuration are propagated throughout the app from the screen to the presented view.

private func setupContactUsButton(
  verticalSizeClass: UIUserInterfaceSizeClass
) {
  //1
  NSLayoutConstraint.deactivate(contactButtonConstraints)

  //2
  if verticalSizeClass == .compact {
    //3
    contactUsButton.setTitle("Contact Us", for: .normal)
    //4
    contactButtonConstraints = [
      contactUsButton.widthAnchor.constraint(
        equalToConstant: 160),
      contactUsButton.heightAnchor.constraint(
        equalToConstant: 40),
      contactUsButton.trailingAnchor.constraint(equalTo:
        view.safeAreaLayoutGuide.trailingAnchor,
        constant: -20),
      contactUsButton.bottomAnchor.constraint(equalTo:
        view.safeAreaLayoutGuide.bottomAnchor, 
        constant: -10),
    ]
  } else {
    //5
    contactUsButton.setTitle("", for: .normal)
    //6
    contactButtonConstraints = [
      contactUsButton.widthAnchor.constraint(
        equalToConstant: 40),
      contactUsButton.heightAnchor.constraint(
        equalToConstant: 40),
      contactUsButton.centerXAnchor.constraint(
        equalTo: view.centerXAnchor),
      contactUsButton.bottomAnchor.constraint(equalTo:
        view.safeAreaLayoutGuide.bottomAnchor, 
        constant: -10),
    ]
  }
  //7
  NSLayoutConstraint.activate(contactButtonConstraints)
}
override func traitCollectionDidChange(_ 
  previousTraitCollection: UITraitCollection?
) {
  //1
  super.traitCollectionDidChange(previousTraitCollection)
  //2
  if traitCollection.verticalSizeClass !=
    previousTraitCollection?.verticalSizeClass {
      //3
      setupContactUsButton(
        verticalSizeClass: traitCollection.verticalSizeClass)
  }
}
//1
view.addSubview(contactUsButton)

//2
setupContactUsButton(
  verticalSizeClass: traitCollection.verticalSizeClass)

Adaptive presentation

A view controller can be presented in different ways, depending on the environment. By default, the system will try to accommodate the view controller, but you can decide how you want your view controller to adapt.

import UIKit

class SettingsTableViewController: UITableViewController {
}
//1
override func awakeFromNib() {
  super.awakeFromNib()
  
  //2
  navigationItem.leftBarButtonItem = UIBarButtonItem(
    barButtonSystemItem: .done,
    target: self,
    action: #selector(dismissModal))
  
  //3
  modalPresentationStyle = .popover
  //4
  popoverPresentationController!.delegate = self
}
@objc private func dismissModal() {
  dismiss(animated: true)
}
extension SettingsTableViewController: 
  UIPopoverPresentationControllerDelegate {
  //1
  func adaptivePresentationStyle(
    for controller: UIPresentationController
  ) -> UIModalPresentationStyle {
    //2
    switch (
      traitCollection.horizontalSizeClass, 
      traitCollection.verticalSizeClass) {
    //3
    case (.compact, .compact):
      return .fullScreen
    default:
      return .none
    }
  }
  
  //4
  func presentationController(
    _ controller: UIPresentationController, 
    viewControllerForAdaptivePresentationStyle
      style: UIModalPresentationStyle
  ) -> UIViewController? {
    //5
    return UINavigationController(
      rootViewController: controller.presentedViewController)
  }
}

UIKit and adaptive interfaces

UIKit provides tools to make adaptable user interfaces. Some of these tools include:

The split view controller

The split view controller acts as a container view controller that manages two child view controllers. If you’ve used the Settings app on an iPad, you may have noticed that the experience is different from what you have on an iPhone 6, for example. The app changes to display the information in a master-detail configuration. This happens thanks to the split view controller.

override func viewDidLoad() {
  super.viewDidLoad()
  
  //1
  guard 
    let leftNavController = 
      viewControllers.first as? UINavigationController,
    let masterViewController = 
      leftNavController.viewControllers.first 
      as? ContactListTableViewController,
    let detailViewController = (viewControllers.last 
      as? UINavigationController)?.topViewController 
      as? MessagesViewController
    else { fatalError() }
  
  //2
  let firstContact = masterViewController.contacts.first
  detailViewController.contact = firstContact
  //3
  masterViewController.delegate = detailViewController
  //4
  detailViewController.navigationItem
    .leftItemsSupplementBackButton = true
  detailViewController.navigationItem
    .leftBarButtonItem = displayModeButtonItem
}

protocol ContactListTableViewControllerDelegate: class {
  func contactListTableViewController(
    _ contactListTableViewController: 
      ContactListTableViewController,
    didSelectContact selectedContact: Contact
  )
}
weak var delegate: ContactListTableViewControllerDelegate?
override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  //1
  guard
    let messagesViewController = 
      delegate as? MessagesViewController,
    let messagesNavigationController = 
      messagesViewController.navigationController 
    else {
      return
  }

  //2
  let selectedContact = contacts[indexPath.row]
  messagesViewController.contactListTableViewController(
    self, 
    didSelectContact: selectedContact)

  //3
  splitViewController?.showDetailViewController(
    messagesNavigationController, 
    sender: nil)
}
extension MessagesViewController: 
  ContactListTableViewControllerDelegate {
    func contactListTableViewController(
      _ contactListTableViewController: 
        ContactListTableViewController,
      didSelectContact selectedContact: Contact
    ) {
      contact = selectedContact
  }
}

Use your layout guides

The system comes with predefined layout guides that can make apps adapt better to different devices. One clear example is the Safe Area Layout Guide that helps prevent content from getting behind the iPhone X notch. You can learn more about this in Chapter 7, “Layout Guides”.

UIAppeareance

UIAppeareance serves as a proxy to have access to the mutable appearance of some classes, like UINavigationBar, UIButton and UIBarButtonItem. By changing the attributes for these classes, you can create consistent themes that you can use throughout the app.

//1
let verticalRegularTrait = 
  UITraitCollection(verticalSizeClass: .regular)
//2
let regularAppearance = 
  UINavigationBar.appearance(for: verticalRegularTrait)
let regularFont = UIFont.systemFont(ofSize: 20)
//3
regularAppearance.titleTextAttributes = 
  [NSAttributedString.Key.font: regularFont]

//4
let verticalCompactTrait = 
  UITraitCollection(verticalSizeClass: .compact)
//5
let compactAppearance = 
  UINavigationBar.appearance(for: verticalCompactTrait)
let compactFont = UIFont.systemFont(ofSize: 14)
//6
compactAppearance.titleTextAttributes = 
  [NSAttributedString.Key.font: compactFont]

Adaptive images

Image assets should be adaptive, too. In this section, you’ll use Asset Catalogs to manage images and provide different versions of them depending on the size class. Also, you’ll explore how the alignment and slicing tool can help you select parts of an image and indicate how an image should resize when necessary.

Images and traits

Asset catalogs give you the possibility of having multiple images depending on the trait environment. You can have different image assets for different size classes.

Alignment insets and slicing

Using the Asset Catalog, you can indicate which parts of an image you want to use so that your app looks good in different scenarios. For this, you need to use alignment insets and slicing.

button.setBackgroundImage(
  UIImage(named: "button-background"), 
  for: .normal)

Challenge

The About screen currently has constraints for Compact Width/Regular Height and Compact Width/Compact Height traits. Your challenge is to add constraints using the Regular Width and Regular Height traits, so the user interfaces look good on more devices.

Key points

  • Size Classes determine how the user interface is laid out.
  • You can modify how view controllers are presented using UIPopoverPresentationControllerDelegate.
  • UIKit provides tools that help you create adaptive interfaces such as UISplitViewController, UIAppeareance proxy and Layout Guides.
  • You can have different images for specific size classes.
  • Split View Controller is a handy tool that comes with UIKit; you can use it to display master-detail like layouts.
  • Use the UIAppeareance proxy when you want to have a consistent user interface attributes across the entire app.
  • You can use Alignment and Slicing to control how your images stretch and to show specific portions when desired.
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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now