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.