The AppCoordinator class is responsible for navigation. It defines the flow of the application and instantiates the view controllers of the project. View controllers no longer need to worry about navigation, which makes them focused and lightweight. There is a cost to this shift in responsibilities, though. As the project grows, the complexity of the AppCoordinator class increases.

The solution is simple, creating multiple coordinators. We don't need to stick to a single coordinator. A typical project has dozens of view controllers. Each view controller is responsible for a view. The same is true for coordinators. The AppCoordinator class is the brain of the project, but it can delegate tasks to other objects.

In this episode, I introduce you to the concept of child coordinators. A child coordinator is responsible for a subflow of the application. It helps the application coordinator handle navigation in the application.

Benefits and Requirements of Child Coordinators

Before I show you how to implement and integrate a child coordinator, I want to emphasize that the use of child coordinators is optional. The AppCoordinator class handles navigation just fine in the project we're working with. Even though the current implementation isn't ideal since the AppCoordinator class manages state, it's an acceptable tradeoff considering the size of the project. We don't need to complicate the project by introducing additional coordinators.

The coordinator pattern is a simple pattern. It's flexible and easy to adopt. Child coordinators can be integrated in several ways and it's your task to decide how you implement and integrate them in a project. The approach I show in this episode sticks to a few rules or requirements.

The first requirement is that a child coordinator is unaware of its parent coordinator. The parent coordinator creates the child coordinator and keeps a reference to it. The child coordinator doesn't keep a reference to its parent coordinator, the coordinator that created it. We revisit this requirement in more detail later in this episode.

The second requirement is that a view controller is unaware of the coordinator that created it. This is similar to how a child coordinator is unaware of its parent coordinator. The previous episodes have illustrated this. The view controller indirectly communicates with a coordinator through handlers or delegation. The view controller is unaware of the coordinator.

Defining a Subflow

A child coordinator is responsible for a subflow of the application. The subflow we focus on in this episode is the purchase flow. We refactored the purchase flow in the previous episode. The drawback of the current implementation is that the AppCoordinator class manages state. The AppCoordinator class keeps track of the photo the user is intending to buy. The goal is to create a child coordinator whose sole responsibility it is to manage the purchase flow.

Creating a Child Coordinator

Let's start by creating a new Swift file for the child coordinator. Create a group in the Coordinators group and name it Child Coordinators. Add a new Swift file to the Child Coordinators group and name it BuyCoordinator.swift. Add an import statement for the UIKit framework and define a class with name BuyCoordinator.

import UIKit

class BuyCoordinator {

}

The buy coordinator has a few dependencies, a navigation controller it can use to present view controllers and the photo the user is intending to buy. Define a private, constant property, navigationController, of type UINavigationController and a private, constant property, photo, of type Photo.

import UIKit

class BuyCoordinator {

    // MARK: - Properties

    private let navigationController: UINavigationController

    // MARK: -

    private let photo: Photo

}

We inject the dependencies of the buy coordinator using initializer injection. We define an initializer that accepts a UINavigationController instance and a Photo instance. The coordinator keeps a reference to the navigation controller in its navigationController property and the Photo instance is assigned to its photo property.

import UIKit

class BuyCoordinator {

    // MARK: - Properties

    private let navigationController: UINavigationController

    // MARK: -

    private let photo: Photo

    // MARK: - Initialization

    init(navigationController: UINavigationController, photo: Photo) {
        // Set Navigation Controller
        self.navigationController = navigationController

        // Set Photo
        self.photo = photo
    }

}

With the foundation of the BuyCoordinator class in place, we can put it to use in the AppCoordinator class. Open AppCoordinator.swift. Navigate to the buyPhoto(_:) method and initialize an instance of the BuyCoordinator class.

private func buyPhoto(_ photo: Photo) {
    // Initialize Buy Coordinator
    let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)

    ...

}

To initiate the purchase flow, we invoke a method with name start() on the buy coordinator. This is similar to how the AppCoordinator class initiates the application flow.

private func buyPhoto(_ photo: Photo) {
    // Initialize Buy Coordinator
    let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)

    // Start Buy Coordinator
    buyCoordinator.start()

    ...

}

Open BuyCoordinator.swift and define a method with name start().

// MARK: - Public API

func start() {

}

We don't need to write a lot of new code. The AppCoordinator class already contains the ingredients we need to implement the BuyCoordinator class. Open AppCoordinator.swift on the left and BuyCoordinator.swift in the assistant editor on the right.

Navigate to the showPhoto(_:) method in AppCoordinator.swift. When the didBuyPhoto handler of the photo view controller is executed, the buyPhoto(_:) method needs to be invoked. We no longer need to store the photo the user is intending to buy in the isBuyingPhoto property.

private func showPhoto(_ photo: Photo) {
    // Initialize Photo View Controller
    let photoViewController = PhotoViewController.instantiate()

    // Configure Photo View Controller
    photoViewController.photo = photo

    // Install Handlers
    photoViewController.didBuyPhoto = { [weak self] (photo) in
        self?.buyPhoto(photo)

        if UserDefaults.isSignedIn {
            self?.buyPhoto(photo)
        } else {
            self?.showSignIn(style: .push)
        }
    }

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

The if statement of the didBuyPhoto handler becomes obsolete because the AppCoordinator class isn't responsible for the purchase flow. Move the if statement to the start() method of the BuyCoordinator class.

private func showPhoto(_ photo: Photo) {
    // Initialize Photo View Controller
    let photoViewController = PhotoViewController.instantiate()

    // Configure Photo View Controller
    photoViewController.photo = photo

    // Install Handlers
    photoViewController.didBuyPhoto = { [weak self] (photo) in
        self?.buyPhoto(photo)
    }

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

We can clean up the if statement by removing the explicit references to the buy coordinator and we no longer need to pass the presentation style to the showSignIn() method.

// MARK: - Public API

func start() {
    if UserDefaults.isSignedIn {
        buyPhoto(photo)
    } else {
        showSignIn()
    }
}

We need to implement the buyPhoto(_:) method and the showSignIn(_:) method. Let's start with the showSignIn() method. We use the implementation of the AppCoordinator class as a starting point.

// MARK: - Helper Methods

private func showSignIn(style: PresentationStyle) {
    // Initialize Sign In View Controller
    let signInViewController = SignInViewController.instantiate()

    // Install Handlers
    signInViewController.didSignIn = { [weak self] (token) in
        // Update User Defaults
        UserDefaults.token = token

        if let photo = self?.isBuyingPhoto {
            // Buy Photo
            self?.buyPhoto(photo)
        } else {
            // Dismiss View Controller
            self?.navigationController.dismiss(animated: true)
        }
    }

    signInViewController.didCancel = { [weak self] in
        self?.navigationController.dismiss(animated: true)
    }

    switch style {
    case .present:
        navigationController.present(signInViewController, animated: true)
    case .push:
        navigationController.pushViewController(signInViewController, animated: true)
    }
}

We need to make four changes. First, we remove the style parameter because we don't need it. Second, we define a local constant with name photo and assign the value of the photo property to it. This makes the implementation of the didSignIn handler easier. Third, we need to update the closure assigned to the didSignIn handler. When the user successfully signs in, the coordinator resumes the purchase flow by invoking the buyPhoto(_:) method. Fourth, the sign in view controller is shown to the user by pushing it onto the navigation stack of the coordinator's navigation controller.

// MARK: - Helper Methods

private func showSignIn() {
    // Initialize Sign In View Controller
    let signInViewController = SignInViewController.instantiate()

    // Helpers
    let photo = self.photo

    // Install Handlers
    signInViewController.didSignIn = { [weak self] (token) in
        // Update User Defaults
        UserDefaults.token = token

        // Buy Photo
        self?.buyPhoto(photo)
    }

    signInViewController.didCancel = { [weak self] in
        self?.navigationController.dismiss(animated: true)
    }

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

There's one other issue we need to address. We shouldn't invoke the dismiss(animated:) method on the navigation controller in the didCancel handler. Let's instead invoke a helper method, finish(), in the didCancel handler. We implement the finish() method later.

// MARK: - Private API

private func finish() {

}

// MARK: - Helper Methods

private func showSignIn() {
    // Initialize Sign In View Controller
    let signInViewController = SignInViewController.instantiate()

    // Helpers
    let photo = self.photo

    // Install Handlers
    signInViewController.didSignIn = { [weak self] (token) in
        // Update User Defaults
        UserDefaults.token = token

        // Buy Photo
        self?.buyPhoto(photo)
    }

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

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

Let's continue by implementing the buyPhoto(_:) method. We use the implementation of the AppCoordinator class as a starting point.

private func buyPhoto(_ photo: Photo) {
    // Initialize Buy Coordinator
    let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)

    // Start Buy Coordinator
    buyCoordinator.start()

    // Initialize Buy View Controller
    let buyViewController = BuyViewController.instantiate()

    // Configure Buy View Controller
    buyViewController.photo = photo

    // Install Handlers
    buyViewController.didBuyPhoto = { [weak self] _ in
        // Reset Helper
        self?.isBuyingPhoto = nil

        // Update User Defaults
        UserDefaults.buy(photo: photo)

        if let viewController = self?.navigationController.viewControllers.first(where: { $0 is PhotoViewController }) {
            self?.navigationController.popToViewController(viewController, animated: true)
        } else {
            self?.navigationController.popToRootViewController(animated: true)
        }
    }

    buyViewController.didCancel = { [weak self] in
        // Reset Helper
        self?.isBuyingPhoto = nil

        if let viewController = self?.navigationController.viewControllers.first(where: { $0 is PhotoViewController }) {
            self?.navigationController.popToViewController(viewController, animated: true)
        } else {
            self?.navigationController.popToRootViewController(animated: true)
        }
    }

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

We start by removing any references to the BuyCoordinator class. The closure assigned to the didBuyPhoto handler can be simplified drastically. We update the user defaults database and invoke the finish() method. In the closure assigned to the didCancel handler, we invoke the finish() method. We remove any references to the isBuyingPhoto property.

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

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

Before we test the implementation of the BuyCoordinator class, we need to clean up the AppCoordinator class. Open AppCoordinator.swift and remove the isBuyingPhoto property. The AppCoordinator class no longer manages state and that's a welcome change. The BuyCoordinator class is responsible for managing the purchase flow. It makes sense that the BuyCoordinator class is aware of the photo the user is intending to buy.

import UIKit
import Foundation

class AppCoordinator {

    // MARK: - Types

    private enum PresentationStyle {
        case present
        case push
    }

    // MARK: - Properties

    private let navigationController = UINavigationController()

    // MARK: - Public API

    var rootViewController: UIViewController {
        return navigationController
    }

    // MARK: -

    func start() {
        showPhotos()
    }

    ...

}

The isBuyingPhoto property is referenced in the showSignIn(_:) method and in the buyPhoto(_:) method. Let's update these methods. When the didSignIn handler of the sign in view controller is invoked, the sign in view controller needs to be dismissed. This means that we only keep the else clause of the if statement.

private func showSignIn(style: PresentationStyle) {
    // Initialize Sign In View Controller
    let signInViewController = SignInViewController.instantiate()

    // Install Handlers
    signInViewController.didSignIn = { [weak self] (token) in
        // Update User Defaults
        UserDefaults.token = token

        // Dismiss View Controller
        self?.navigationController.dismiss(animated: true)
    }

    signInViewController.didCancel = { [weak self] in
        self?.navigationController.dismiss(animated: true)
    }

    switch style {
    case .present:
        navigationController.present(signInViewController, animated: true)
    case .push:
        navigationController.pushViewController(signInViewController, animated: true)
    }
}

Updating the implementation of the buyPhoto(_:) method is simple. The method creates an instance of the BuyCoordinator class and invokes its start() method. That's it. We can remove the rest of the implementation.

private func buyPhoto(_ photo: Photo) {
    // Initialize Buy Coordinator
    let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)

    // Start Buy Coordinator
    buyCoordinator.start()
}

We can also remove the PresentationStyle enum. We no longer need to specify how the sign in view controller is presented to the user.

import UIKit
import Foundation

class AppCoordinator {

    // MARK: - Properties

    private let navigationController = UINavigationController()

    // MARK: - Public API

    var rootViewController: UIViewController {
        return navigationController
    }

    // MARK: -

    func start() {
        showPhotos()
    }

    ...

}

This means that the showSignIn(_:) method can be simplified even more. The method no longer accepts an argument of type PresentationStyle and we don't need to switch on the style parameter. The sign in view controller is presented to the user by invoking the present(_:animated:completion:) method on the coordinator's navigation controller.

private func showSignIn() {
    // Initialize Sign In View Controller
    let signInViewController = SignInViewController.instantiate()

    // Install Handlers
    signInViewController.didSignIn = { [weak self] (token) in
        // Update User Defaults
        UserDefaults.token = token

        // Dismiss View Controller
        self?.navigationController.dismiss(animated: true)
    }

    signInViewController.didCancel = { [weak self] in
        self?.navigationController.dismiss(animated: true)
    }

    // Present Sign In View Controller
    navigationController.present(signInViewController, animated: true)
}

There's one more compiler error we need to fix. We no longer need to pass the presentation style to the showSignIn() method in the showPhotos() method.

private func showPhotos() {
    // Initialize Photos View Controller
    let photosViewController = PhotosViewController.instantiate()

    // Install Handlers
    photosViewController.didSignIn = { [weak self] in
        self?.showSignIn()
    }

    photosViewController.didSelectPhoto = { [weak self] (photo) in
        self?.showPhoto(photo)
    }

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

Build and Run

Build and run the application to test the implementation. We need to make sure we haven't introduced any regressions. Make sure the user is signed out. Tap the Sign In button in the top right. The sign in view controller is presented. Tap the Sign In button below the text fields to sign in. That works fine. Tap the Sign Out button to sign out.

Let's test the purchase flow. Tap a photo in the table view. This takes the user to the detail view of the photo. Tap the Buy button in the top right to initiate the purchase flow. The application shows the sign in view. Tap the Sign In button below the text fields to sign in. The user appears to be signed in, but nothing happens. The purchase flow doesn't resume. Something's not quite right.

What's Next?

We have drastically reduced the complexity of the AppCoordinator class by introducing a child coordinator. That's a welcome improvement, but we have introduced a regression we need to resolve. The introduction of the buy coordinator has broken the purchase flow. We restore the purchase flow in the next episode.