In the previous episodes, we added support for a vertical purchase flow. Most applications are a puzzle of horizontal and vertical flows. Combining horizontal and vertical flows allows for flexible and dynamic application flows.

The current implementation of the coordinator pattern has a limitation I would like to remove in this episode. The application coordinator is the only coordinator that can manage child coordinators. That ability shouldn't be restricted to the application coordinator.

The plan of action is simple. By moving the ability to manage child coordinators to the Coordinator class, any coordinator inheriting form the Coordinator class automatically has the ability to push and pop child coordinators.

To test the changes we make in this episode, we add a new subflow to the application. The goal is to initiate a vertical flow from the buy view controller. Remember that the buy view controller is managed by the buy coordinator. Let me show you what we start with.

Exploring the Terms View Controller

The only difference with the finished project of the previous episode is the addition of the TermsViewController class. Open TermsViewController.swift in the Terms View Controller group, a subgroup of the View Controllers group. The TermsViewController class conforms to the Storyboardable protocol and its implementation is basic. It defines a handler with name didCancel and a cancel(_:) method. The didCancel property is of an optional type, a closure that accepts no arguments. The didCancel handler is invoked in the cancel(_:) method. That's it.

import UIKit

class TermsViewController: UIViewController, Storyboardable {

    // MARK: - Properties

    var didCancel: (() -> Void)?

    // MARK: - Actions

    @IBAction func cancel(_ sender: Any) {
        didCancel?()
    }

}

To understand the purpose of the TermsViewController class, we need to open Main.storyboard. Navigate to the Terms View Controller scene. The terms view controller presents the terms of service to the user. The Cancel button in the top right invokes the cancel(_:) method of the TermsViewController class. That in turn invokes the didCancel handler of the terms view controller.

Terms View Controller

Creating a Plan of Action

The goal of this episode is simple. We add a button with title Terms of Service below the Buy button of the buy view controller. When the user taps the Terms of Service button, the terms view controller is presented modally. To make that possible, we need to take a few simple steps.

First, we add a button to the buy view controller and define a handler to notify the buy coordinator when the user taps the button. Second, we create and implement a Coordinator subclass that manages the vertical flow, that is, presenting the terms view controller. Third, the BuyCoordinator class should be capable of initiating the coordinator that manages the terms view controller. In other words, the BuyCoordinator class should have the ability to manage child coordinators. Let's start with the first step.

Defining a Handler

Open BuyViewController.swift and define a handler with name didShowTerms. The didShowTerms property is of an optional type, a closure that accepts no arguments.

var didShowTerms: (() -> Void)?

We also need to create an action that invokes the didShowTerms handler. Create an action with name showTerms(_:). In the body of the showTerms(_:) action, we invoke the didShowTerms handler.

@IBAction func showTerms(_ sender: Any) {
    didShowTerms?()
}

Open Main.storyboard and navigate to the Buy View Controller scene. Add a button to the vertical stack view, below the Buy button. Set the title of the button to Terms of Service. Select the buy view controller and open the Connections Inspector on the right. Connect the showTerms(_:) action to the Touch Up Inside event of the Terms of Service button.

Buy View Controller

Implementing the Terms Coordinator

The next step is creating and implementing a coordinator that manages the terms view controller. This should be straightforward by now. Add a new Swift file to the Child Coordinators group and name it TermsCoordinator.swift. Add an import statement for the UIKit framework and define a class with name TermsCoordinator. The TermsCoordinator class subclasses the Coordinator class.

import UIKit

class TermsCoordinator: Coordinator {

}

We want to present the terms view controller modally, which means the terms coordinator manages a vertical flow. For that to work, the terms coordinator needs a reference to a view controller that can present the terms view controller. This is similar to how we implemented the vertical purchase flow in the previous episodes. Define a private, constant property, presentingViewController, of type UIViewController.

import UIKit

class TermsCoordinator: Coordinator {

    // MARK: - Properties

    private let presentingViewController: UIViewController

}

The reference to the presenting view controller is passed to the terms coordinator during initialization. We create an initializer that accepts a UIViewController instance as its only argument. In the initializer, we store a reference to the UIViewController instance in the presentingViewController property.

import UIKit

class TermsCoordinator: Coordinator {

    // MARK: - Properties

    private let presentingViewController: UIViewController

    // MARK: - Initialization

    init(presentingViewController: UIViewController) {
        // Set Presenting View Controller
        self.presentingViewController = presentingViewController
    }

}

The next step is overriding the start() method. In the start() method, we invoke a helper method, showTerms().

// MARK: - Overrides

override func start() {
    // Show Terms
    showTerms()
}

The implementation of the showTerms() method should look familiar by now. We instantiate an instance of the TermsViewController class by invoking the instantiate() class method. We install the didCancel handler of the terms view controller. In the closure we assign to the didCancel handler, we invoke another helper method, finish(). We implement the finish() method in a moment. To present the terms view controller, the terms coordinator invokes the present(_:animated:completion:) method on the presenting view controller, passing in a reference to the TermsViewController instance.

// MARK: - Helper Methods

private func showTerms() {
    // Initialize Terms View Controller
    let termsViewController = TermsViewController.instantiate()

    // Install Handlers
    termsViewController.didCancel = { [weak self] in
        self?.finish()
    }

    // Present Terms View Controller
    presentingViewController.present(termsViewController, animated: true)
}

The finish() method is declared privately. Its implementation should also look familiar. In the body of the finish() method, the terms coordinator invokes the dismiss(animated:completion:) method on the presenting view controller and it invokes the didFinish handler to notify the parent coordinator of the terms coordinator.

// MARK: - Private API

private func finish() {
    // Dismiss Terms View Controller
    presentingViewController.dismiss(animated: true)

    // Invoke Handler
    didFinish?(self)
}

That's it. I hope you agree that the implementation of the TermsCoordinator class isn't complex. Notice that the terms coordinator doesn't manage a navigation controller. Because it presents a single view controller, there's no need for a navigation controller. If the TermsCoordinator class were to manage a vertical flow with multiple view controllers, then we would need a navigation controller.

Updating the Coordinator Class

Only the AppCoordinator class is currently capable of managing child coordinators. This is easy to change, though. Open Coordinator.swift on the left and AppCoordinator.swift in the assistant editor on the right. We need to make two changes. First, move the childCoordinators property from the AppCoordinator class to the Coordinator class and remove the private keyword.

import UIKit

class Coordinator: NSObject, UINavigationControllerDelegate {

    // MARK: - Properties

    var didFinish: ((Coordinator) -> Void)?

    // MARK: -

    var childCoordinators: [Coordinator] = []

    // MARK: - Methods

    func start() {}

    // MARK: -

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {}
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {}

}

Second, move pushCoordinator(_:) and popCoordinator(_:) from the AppCoordinator class to the Coordinator class and remove the private keywords.

import UIKit

class Coordinator: NSObject, UINavigationControllerDelegate {

    // MARK: - Properties

    var didFinish: ((Coordinator) -> Void)?

    // MARK: -

    var childCoordinators: [Coordinator] = []

    // MARK: - Methods

    func start() {}

    // MARK: -

    func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {}
    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {}

    // MARK: -

    func pushCoordinator(_ coordinator: Coordinator) {
        // Install Handler
        coordinator.didFinish = { [weak self] (coordinator) in
            self?.popCoordinator(coordinator)
        }

        // Start Coordinator
        coordinator.start()

        // Append to Child Coordinators
        childCoordinators.append(coordinator)
    }

    func popCoordinator(_ coordinator: Coordinator) {
        // Remove Coordinator From Child Coordinators
        if let index = childCoordinators.firstIndex(where: { $0 === coordinator }) {
            childCoordinators.remove(at: index)
        }
    }

}

These are the only changes we need to make. Any Coordinator subclass is now capable of managing one or more child coordinators. Build the project to make sure we didn't break anything. The compiler doesn't throw any errors, which is a good sign.

Presenting the Terms View Controller

The last step is presenting the terms view controller when the user taps the Terms of Service button of the buy view controller. Open BuyCoordinator.swift and navigate to the buyPhoto(_:) method. We need to install the didShowTerms handler of the buy view controller. In the closure we assign to the didShowTerms handler, we invoke a helper method, showTerms().

private func buyPhoto(_ photo: Photo) {
    // Initialize Buy View Controller
    let buyViewController = BuyViewController.instantiate()

    // Configure Buy View Controller
    buyViewController.photo = photo

    // Install Handlers
    buyViewController.didBuyPhoto = { [weak self] _ in
        // Update User Defaults
        UserDefaults.buy(photo: photo)

        // Finish
        self?.finish()
    }

    buyViewController.didCancel = { [weak self] in
        self?.finish()
    }

    buyViewController.didShowTerms = { [weak self] in
        self?.showTerms()
    }

    // Push Buy View Controller Onto Navigation Stack
    navigationController.pushViewController(buyViewController, animated: true)
}

The implementation of the showTerms() method is straightforward. We initialize an instance of the TermsCoordinator class, passing the navigation controller of the buy coordinator to the initializer. The navigation controller is the presenting view controller of the terms coordinator. Because the TermsCoordinator class is a Coordinator subclass, we can invoke the pushCoordinator(_:) method, passing in the TermsCoordinator instance. That's it. The details are handled by the Coordinator class.

private func showTerms() {
    // Initialize Terms Coordinator
    let termsCoordinator = TermsCoordinator(presentingViewController: navigationController)

    // Push Terms Coordinator
    pushCoordinator(termsCoordinator)
}

Build and run the application to see the result. Make sure the user is signed out. Select a photo in the table view of the photos view controller and tap the Buy button in the top right to initiate the purchase flow. Sign in and tap the Terms of Service button of the buy view controller to present the terms view controller. The purchase flow is presented as a horizontal flow. The buy coordinator initiates the terms flow as a vertical flow. This shows that the solution we implemented works as expected.

Let's test the vertical purchase flow. Tap the Cancel button of the terms view controller to return to the buy view controller. Tap the Cancel button of the buy view controller to cancel the purchase flow. Return to the photos view controller and tap the Sign Out button in the top right to sign out. Tap the Buy button of a table view cell in the table view to initiate the vertical purchase flow. Sign in and tap the Terms of Service button of the buy view controller to present the terms view controller. Presenting the terms view controller from the vertical purchase flow also works as expected.

What's Next?

We successfully added the ability to the Coordinator class to push and pop child coordinators. Every Coordinator subclass is now capable of initiating subflows.