View controller containment is an indispensable pattern in iOS projects. Several key components of the UIKit framework take advantage of view controller containment, including the UINavigationController class, the UITabBarController class, and the UISplitViewController class. As I mentioned earlier in this series, view controller containment is a pattern I adopt in every iOS project and Rainstorm is no exception.

The idea is simple. A parent or container view controller contains one or more child view controllers. View controller containment ensures that each child view controller is notified of important view life cycle events. The root view controller we added in the previous episode acts as the parent or container view controller.

The user interface of Rainstorm is simple. The application displays the current weather conditions at the top and it shows the user a weather forecast of the coming days at the bottom. We could include both views as subviews of the root view controller's view, but that would result in a fat view controller. It would add complexity we can avoid with ease. View controller containment is straightforward to implement and it helps to keep view controllers lightweight and focused.

Let's start by creating a class for each of the child view controllers.

Day View Controller

Create a group in the View Controllers group and name it Weather View Controllers. Create a group in the Weather View Controllers group and name it Day View Controller.

Creating Groups to Keep the Project Organized

Create a new file in the Day View Controller group by choosing the Cocoa Touch Class template from the iOS section.

Creating the Day View Controller

Name the class DayViewController and set its subclass to UIViewController. There's no need to create a XIB file. Make sure Language is set to Swift.

Creating the Day View Controller

The DayViewController class is responsible for showing the current weather conditions at the top of the user interface. Remove the implementation of the DayViewController class with the exception of the viewDidLoad() method.

We mark the DayViewController class as final, which means it cannot be subclassed. It also results in a tiny performance improvement.

import UIKit

final class DayViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

I always keep the viewDidLoad() method short to make sure it's easy to see what's happening. The first helper method I create in almost every view controller is setupView(). In this private helper method, we configure the view and its subviews. Let's give the view controller's view a bright background color to make sure it stands out.

import UIKit

final class DayViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup View
        setupView()
    }

    // MARK: - View Methods

    private func setupView() {
        // Configure View
        view.backgroundColor = .green
    }

}

Week View Controller

It's time to create the view controller that shows the user a weather forecast of the coming days. Create a group in the Weather View Controllers group and name it Week View Controller.

Creating Groups to Keep the Project Organized

Create a new file and choose the Cocoa Touch Class template from the iOS section.

Creating the Week View Controller

Name the class WeekViewController and set its subclass to UIViewController. There's no need to create a XIB file. Make sure Language is set to Swift.

Creating the Week View Controller

Remove the implementation of the WeekViewController class with the exception of the viewDidLoad() method. We mark the WeekViewController class as final.

import UIKit

final class WeekViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

We take the same approach in the WeekViewController class. We implement a private helper method, setupView(), in which we configure the view controller's view. We set its background color to a bright red color. We invoke setupView() in the view controller's viewDidLoad() method.

import UIKit

final class WeekViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Setup View
        setupView()
    }

    // MARK: - View Methods

    private func setupView() {
        // Configure View
        view.backgroundColor = .red
    }

}

View Controller Containment

I won't cover view controller containment in detail in this series. If you'd like to learn more about view controller containment, I recommend reading Managing View Controllers With Container View Controllers. You can find it on the Cocoacasts website. Even though Interface Builder supports view controller containment, in this series I show you how to adopt the pattern in code.

Let's start by adding the day and week view controllers to the main storyboard. Open Main.storyboard and drag a view controller from the Object Library to the canvas. With the view controller selected, open the Identity Inspector on the right and set Class and Storyboard ID to DayViewController. We need the storyboard identifier to load the view controller from the storyboard.

Adding the Day View Controller to the Main Storyboard

We add another view controller to the storyboard. In the Identity Inspector, we set Class and Storyboard ID to WeekViewController.

Adding the Week View Controller to the Main Storyboard

Before we revisit the RootViewController class, I want to make sure we don't need to rely on string literals to load the view controllers from the storyboard. Let me show you what I have in mind.

Create a new group, Extensions, and add a new Swift file to the group. Name the file UIViewController.swift.

Creating an Extension for UIViewController

Add an import statement for UIKit and create an extension for the UIViewController class.

import UIKit

extension UIViewController {

}

To avoid string literals in the codebase, a UIViewController subclass should be able to return its storyboard identifier. We declare a static computed property, storyboardIdentifier, of type String. The computed property returns a string that is equal to the name of the class. Every UIViewController subclass now has a storyboard identifier we can use. This solution only works if the storyboard identifier in the storyboard is equal to the name of the class.

import UIKit

extension UIViewController {

    // MARK: - Static Properties

    static var storyboardIdentifier: String {
        return String(describing: self)
    }

}

Create another Swift file in the Extensions group and name it UIStoryboard.swift. Add an import statement for UIKit and create an extension for the UIStoryboard class.

Creating an Extension for UIStoryboard

We implement a static computed property, main, for accessing the main storyboard. This ensures we only need to use a string literal for instantiating the main storyboard in one location.

import UIKit

extension UIStoryboard {

    // MARK: - Static Properties

    static var main: UIStoryboard {
        return UIStoryboard(name: "Main", bundle: Bundle.main)
    }

}

Both of these improvements are small, but I can assure you that they add up. The fewer string literals you have in a project the better. That's my experience.

Open RootViewController.swift. We need to declare a stored property for each child view controller. Let's start with the day view controller. Declare a private constant property, dayViewController, of type DayViewController. The implementation is surprisingly simple thanks to the extensions we created a moment ago.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    private let dayViewController: DayViewController = {
        guard let dayViewController = UIStoryboard.main.instantiateViewController(withIdentifier: DayViewController.storyboardIdentifier) as? DayViewController else {
            fatalError("Unable to Instantiate Day View Controller")
        }

        // Configure Day View Controller
        dayViewController.view.translatesAutoresizingMaskIntoConstraints = false

        return dayViewController
    }()

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

Instantiating the day view controller should never fail. That's why I use a guard statement and throw a fatal error if the application is unable to instantiate the day view controller. We set the translatesAutoresizingMaskIntoConstraints property of the view of the day view controller to false because I plan to use Auto Layout to size and position the view.

Notice that we instantiate the DayViewController instance in a closure. This technique is useful if the initialization and configuration of the instance requires a few lines of code. It's important that we append a pair of parentheses to the closure to invoke it. Because the closure is executed during the initialization of the RootViewController instance we can declare the property as a constant. We could declare the property as a lazy variable property, but that wouldn't gain us anything in this scenario. I prefer to declare stored properties as constants whenever possible because that subtle detail tells me its value won't change once it has a value.

We define another private constant property, weekViewController, of type WeekViewController. The implementation is almost identical.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    private let dayViewController: DayViewController = {
        guard let dayViewController = UIStoryboard.main.instantiateViewController(withIdentifier: DayViewController.storyboardIdentifier) as? DayViewController else {
            fatalError("Unable to Instantiate Day View Controller")
        }

        // Configure Day View Controller
        dayViewController.view.translatesAutoresizingMaskIntoConstraints = false

        return dayViewController
    }()

    private let weekViewController: WeekViewController = {
        guard let weekViewController = UIStoryboard.main.instantiateViewController(withIdentifier: WeekViewController.storyboardIdentifier) as? WeekViewController else {
            fatalError("Unable to Instantiate Week View Controller")
        }

        // Configure Week View Controller
        weekViewController.view.translatesAutoresizingMaskIntoConstraints = false

        return weekViewController
    }()

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

In viewDidLoad(), we invoke a helper method, setupChildViewControllers(). In this private helper method, we add the day and week view controllers to the root view controller, the parent or container view controller of the day and week view controller. The implementation shows the steps that are required to add a child view controller to a container view controller.

// MARK: - View Life Cycle

override func viewDidLoad() {
    super.viewDidLoad()

    // Setup Child View Controllers
    setupChildViewControllers()
}

First, we add each child view controller to the container view controller by invoking addChildViewController(_:) on the container view controller, passing in the child view controller as an argument. By invoking this method, the child view controller automatically receives a message of willMove(toParentViewController:) with the container view controller as the only argument.

// Add Child View Controllers
addChildViewController(dayViewController)
addChildViewController(weekViewController)

Second, we add the view of each child view controller to the view of the container view controller. Even though the container view controller is responsible for the size and position of the child view controller's view, the view hierarchy of the child view controller continues to be the responsibility of the child view controller.

// Add Child View as Subview
view.addSubview(dayViewController.view)
view.addSubview(weekViewController.view)

We use Auto Layout to size and position the view of each child view controller. Using Auto Layout in code used to be tedious, but Auto Layout anchors have made this a little less daunting. The syntax may look a bit odd if you're new to Auto Layout anchors. We pin the day view controller's view to the top, left, and right edges of its superview and fix the height of the view to 200 points.

// Configure Day View
dayViewController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
dayViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
dayViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
dayViewController.view.heightAnchor.constraint(equalToConstant: 200.0).isActive = true

The week view controller's view is pinned to the bottom of the day view controller's view and to the left, right, and bottom edges of its superview.

// Configure Week View
weekViewController.view.topAnchor.constraint(equalTo: dayViewController.view.bottomAnchor).isActive = true
weekViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
weekViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
weekViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

Third, when the child view controller is added to the container view controller and the child view controller's view is ready to be displayed, the container view controller notifies the child view controller by sending it a message of didMove(toParentViewController:), passing itself as the only argument.

// Notify Child View Controller
dayViewController.didMove(toParentViewController: self)
weekViewController.didMove(toParentViewController: self)

This is what the implementation of the setupChildViewControllers() method looks like.

// MARK: - Helper Methods

private func setupChildViewControllers() {
    // Add Child View Controllers
    addChildViewController(dayViewController)
    addChildViewController(weekViewController)

    // Add Child View as Subview
    view.addSubview(dayViewController.view)
    view.addSubview(weekViewController.view)

    // Configure Day View
    dayViewController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    dayViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    dayViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    dayViewController.view.heightAnchor.constraint(equalToConstant: 200.0).isActive = true

    // Configure Week View
    weekViewController.view.topAnchor.constraint(equalTo: dayViewController.view.bottomAnchor).isActive = true
    weekViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    weekViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    weekViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

    // Notify Child View Controller
    dayViewController.didMove(toParentViewController: self)
    weekViewController.didMove(toParentViewController: self)
}

Run the application in the simulator to see the result. You should see a green view at the top and a red view at the bottom of the user interface.

This is what we have so far.

Getting Rid of Literals

It's no secret that I don't like random string literals scattered across a codebase and this also applies to number literals. We used a number literal to define the height of the day view controller's view. We can get rid of that number literal, but, be warned, it can look a bit odd. Most developers think this is one step too far.

We define an extension for the RootViewController class at the bottom of RootViewController.swift. In the extension, we define an enum, Layout. Notice that we mark the Layout enum with the fileprivate keyword to make sure it's only accessible from within RootViewController.swift. We define another enum inside the Layout enum, DayView. The DayView enum defines a static constant property, height, of type CGFloat. Its value is set to 200.0.

extension RootViewController {

    fileprivate enum Layout {
        enum DayView {
            static let height: CGFloat = 200.0
        }
    }

}

We can use this value in setupChildViewControllers(). It may seem as if this is simply moving the value to another location. The idea, however, is to group related values in one place. Like I said, this is a personal choice and it may feel a bit odd in this context. For more complex user interfaces, however, it can do wonders.

// MARK: - Helper Methods

private func setupChildViewControllers() {
    // Add Child View Controllers
    addChildViewController(dayViewController)
    addChildViewController(weekViewController)

    // Add Child View as Subview
    view.addSubview(dayViewController.view)
    view.addSubview(weekViewController.view)

    // Configure Day View
    dayViewController.view.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
    dayViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    dayViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    dayViewController.view.heightAnchor.constraint(equalToConstant: Layout.DayView.height).isActive = true

    // Configure Week View
    weekViewController.view.topAnchor.constraint(equalTo: dayViewController.view.bottomAnchor).isActive = true
    weekViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
    weekViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
    weekViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

    // Notify Child View Controller
    dayViewController.didMove(toParentViewController: self)
    weekViewController.didMove(toParentViewController: self)
}

With the day and week view controllers in place, it's time to commit your changes. I won't cover source control in detail in this series.

If you'd like to learn more about view controller containment, I recommend reading Managing View Controllers With Container View Controllers. You can find it on the Cocoacasts website. In the next episode, we fetch weather data from the Dark Sky API.