I hope this series has convinced you of the value of the coordinator pattern. You should have a good understanding of the pattern by now and be able to adopt it in a project. In the remainder of this series, we cover more advanced aspects of the coordinator pattern. In this and the next episodes, we zoom in on horizontal and vertical flows. Let's start by discussing the differences between horizontal and vertical flows.

Horizontal and Vertical Flows

When the user taps a photo in the table view of the photos view controller, a detail view is shown to the user by pushing a photo view controller onto the navigation stack. That's an example of a horizontal flow. When the user taps the Buy button in the top right, the purchase flow is initiated. The purchase flow extends the current flow, the flow that triggered the purchase flow. The application coordinator passes its navigation controller to the buy coordinator, which pushes the view controllers of the purchase flow onto the navigation stack of the navigation controller. The buy coordinator extends the horizontal flow of the application coordinator.

The flow of a typical application isn't strictly horizontal. View controllers are pushed and popped from a navigation stack, but they are also presented and dismissed modally. In this and the next episodes, we add the ability to present the purchase flow modally. By presenting the purchase flow modally, the buy coordinator no longer extends the horizontal flow of the application coordinator. The buy coordinator initiates a vertical flow. It manages a flow that branches off of the horizontal flow of the application coordinator.

Creating a Base Class

Earlier in this series, we created the Coordinator protocol. Even though the Coordinator protocol works fine, moving forward we won't be using a protocol to define a coordinator. Earlier in this series, I presented two options, defining a protocol or creating a base class from which coordinators inherit. To avoid code duplication, we refactor the current implementation by creating a base class from which coordinators inherit. The idea isn't complex. We convert the Coordinator protocol to a class that inherits from NSObject. Let's take it step by step.

We start by updating the project structure. Create a group with name Application Coordinator in the Coordinators group and move AppCoordinator.swift to the Application Coordinator group. Move Coordinator.swift to the Coordinators group and open Coordinator.swift. Replace protocol with class and AnyObject with NSObject. We define a class with name Coordinator that inherits from NSObject.

import UIKit

class Coordinator: NSObject {

    // MARK: - Properties

    var didFinish: ((Coordinator) -> Void)? { get set }

    // MARK: - Methods

    func start()

    // MARK: -

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

}

extension Coordinator {

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

}

Because Coordinator is no longer a protocol, we need to update the didFinish property declaration and add empty implementations for the start(), navigationController(_:willShow:animated:), and navigationController(_:didShow:animated:) methods. We no longer need the extension at the bottom of Coordinator.swift.

import UIKit

class Coordinator: NSObject {

    // MARK: - Properties

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

    // MARK: - Methods

    func start() {}

    // MARK: -

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

}

We conform the Coordinator class to the UINavigationControllerDelegate protocol because a coordinator should be able to act as the delegate of a UINavigationController instance.

import UIKit

class Coordinator: NSObject, UINavigationControllerDelegate {

    // MARK: - Properties

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

    // MARK: - Methods

    func start() {}

    // MARK: -

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

}

With the Coordinator class in place, we can update the AppCoordinator and BuyCoordinator classes. The coordinators of the project should inherit from the Coordinator class.

Updating AppCoordinator

Open AppCoordinator.swift and replace NSObject with Coordinator in the class definition of the AppCoordinator class.

import UIKit
import Foundation

class AppCoordinator: Coordinator {

    ...

}

We need to make a few changes. Prefix the start() method with the override keyword.

override func start() {
    // Set Navigation Controller Delegate
    navigationController.delegate = self

    // Show Photos
    showPhotos()
}

The Coordinator class conforms to the UINavigationControllerDelegate protocol, which means that the AppCoordinator class also conforms to the UINavigationControllerDelegate protocol. The extension at the bottom of AppCoordinator.swift is no longer needed. We need to override navigationController(_:willShow:animated:) and navigationController(_:didShow:animated:), but that isn't possible in an extension. Move these methods to the class definition, below thestart()method, and prefix the function definitions with theoverride` keyword.

// MARK: - Overrides

override func start() {
    // Set Navigation Controller Delegate
    navigationController.delegate = self

    // Show Photos
    showPhotos()
}

// MARK: -

override func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
    childCoordinators.forEach { (childCoordinator) in
        childCoordinator.navigationController(navigationController, willShow: viewController, animated: animated)
    }
}

override func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
    childCoordinators.forEach { (childCoordinator) in
        childCoordinator.navigationController(navigationController, didShow: viewController, animated: animated)
    }
}

Updating BuyCoordinator

Open BuyCoordinator.swift. There's no need to update the class definition. We can remove the didFinish property because it's already defined by the Coordinator class, the superclass of the BuyCoordinator class. We also need to prefix the start() and navigationController(_:didShow:animated:) methods with the override keyword.

import UIKit

class BuyCoordinator: Coordinator {

    // MARK: - Properties

    private let navigationController: UINavigationController

    // MARK: -

    private let photo: Photo

    // MARK: -

    private let initialViewController: UIViewController?

    // MARK: - Initialization

    ...

    // MARK: - Overrides

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

    // MARK: -

    override func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        if viewController === initialViewController {
            didFinish?(self)
        }
    }

    ...

}

Because the BuyCoordinator class inherits from the Coordinator class, we need to invoke the initializer of the superclass in its initializer.

// MARK: - Initialization

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

    // Set Photo
    self.photo = photo

    // Set Initial View Controller
    self.initialViewController = navigationController.viewControllers.last

    super.init()
}

Build and run the application to make sure we didn't break anything. 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 Buy button to complete the purchase. Everything seems to work fine.

Implementing a Vertical Flow

What changes do we need to make to present the purchase flow as a vertical flow, that is, modally. Let's create a separate coordinator for the vertical flow. Create a new Swift file and name it VerticalBuyCoordinator.swift. Add an import statement for the UIKit framework and define a class with name VerticalBuyCoordinator that inherits from the Coordinator class.

import UIKit

class VerticalBuyCoordinator: Coordinator {

}

What does the coordinator need to present and manage the purchase flow? It needs to know which photo the user is intending to buy. Define a private, constant property, photo, of type Photo.

import UIKit

class VerticalBuyCoordinator: Coordinator {

    // MARK: - Properties

    private let photo: Photo

}

It also needs access to a navigation controller. Even though the purchase flow is presented as a vertical flow, the coordinator needs the ability to show the sign in and buy view controllers. Define a private, constant property, navigationController, of type UINavigationController.

import UIKit

class VerticalBuyCoordinator: Coordinator {

    // MARK: - Properties

    private let photo: Photo

    // MARK: -

    private let navigationController: UINavigationController

}

The VerticalBuyCoordinator class is responsible for the presentation of its view controllers. Because it manages a vertical flow, it presents its navigation controller modally. Presenting the navigation controller isn't the responsibility of the parent coordinator. The parent coordinator doesn't need to know how the child coordinator presents its view controllers. It doesn't need to know whether the child coordinator manages a horizontal or a vertical flow. To present the navigation controller modally, the VerticalBuyCoordinator class needs a reference to a view controller that can present its navigation controller. Define a private, constant property, presentingViewController, of type UIViewController.

import UIKit

class VerticalBuyCoordinator: Coordinator {

    // MARK: - Properties

    private let photo: Photo

    // MARK: -

    private let navigationController: UINavigationController

    // MARK: -

    private let presentingViewController: UIViewController

}

With the properties of the VerticalBuyCoordinator class defined, it's time to implement an initializer for the class. The initializer accepts two arguments, a UIViewController instance and a Photo instance. The UIViewController instance is the view controller that presents the coordinator's navigation controller. We store a reference to the view controller in the presentingViewController property. We assign the Photo instance to the photo property of the coordinator.

We don't pass a navigation controller to the initializer. The coordinator is responsible for the creation and configuration of its navigation controller. This is a key difference with the BuyCoordinator class, which receives its navigation controller from its parent coordinator.

Create an instance of the UINavigationController class and store a reference to it in the navigationController property. Before we can set the coordinator as the delegate of the navigation controller, we need to invoke the initializer of the superclass.

// MARK: - Initialization

init(presentingViewController: UIViewController, photo: Photo) {
    // Set Presenting View Controller
    self.presentingViewController = presentingViewController

    // Set Photo
    self.photo = photo

    // Initialize Navigation Controller
    self.navigationController = UINavigationController()

    super.init()

    // Configure Navigation Controller
    navigationController.delegate = self
}

The next step is overriding the start() method. The implementation is similar to that of the BuyCoordinator class. Let's use that implementation as a starting point.

// MARK: - Overrides

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

Copy the buyPhoto(_:) and showSignIn() methods of the BuyCoordinator class to the VerticalBuyCoordinator class.

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

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

The implementation of the start() method is incomplete. Remember that the navigation controller of the VerticalBuyCoordinator class needs to be presented modally. After invoking the buyPhoto(_:) or showSignIn() method, we invoke the present(_:animated:completion:) method on the presenting view controller.

// MARK: - Overrides

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

    // Present Navigation Controller
    presentingViewController.present(navigationController, animated: true)
}

The implementation of the VerticalBuyCoordinator class is almost complete. All that's left is implementing the finish() method. Define a private method with name finish(). In the finish() method, we dismiss the navigation controller of the coordinator by invoking dismiss(animated:completion:) on the presenting view controller. We also invoke the didFinish handler to notify the parent coordinator.

// MARK: - Private API

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

    // Invoke Handler
    didFinish?(self)
}

Testing the Vertical Purchase Flow

Let's test the vertical purchase flow. Open AppCoordinator.swift and navigate to the buyPhoto(_:) method. Instead of creating a BuyCoordinator instance, we create a VerticalBuyCoordinator instance. The presenting view controller we pass to the initializer is the navigation controller of the application coordinator.

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

    // Push Buy Coordinator
    pushCoordinator(buyCoordinator)
}

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. Notice that the sign in view controller is presented modally. It isn't pushed onto the navigation stack of the application coordinator's navigation controller. Sign in and tap the Buy button to complete the purchase. After completing the purchase flow, the navigation controller of the coordinator is dismissed.

Navigate to the photos view controller and select another photo. Tap the Buy button in the top right to initiate the purchase flow. Tap the Cancel button to cancel the purchase flow. The navigation controller of the coordinator is dismissed when the user cancels the purchase flow.

What's Next?

The flow of your application is a puzzle of horizontal and vertical flows. In this episode, we moved from a horizontal to a vertical purchase flow. The implementation of the VerticalBuyCoordinator class looks fine, but I'm sure you noticed that the BuyCoordinator and VerticalBuyCoordinator classes have quite a bit in common. In the next episode, we merge both classes into a single class.