We refactored the AppCoordinator class in the previous episode. The purchase flow is no longer managed by the AppCoordinator class. We created a child coordinator, the BuyCoordinator class, which is responsible for managing the purchase flow.
Even though the behavior of the application shouldn't have changed, we broke the purchase flow by introducing the buy coordinator. We restore the purchase flow in this episode by examining the problem and implementing a solution.
Managing Child Coordinators
The issue is fairly easy to understand if you have a basic grasp of Swift. Open AppCoordinator.swift and navigate to the buyPhoto(_:) method. We create a BuyCoordinator instance and invoke the start() method to initiate the purchase flow. Because the user isn't signed in, a SignInViewController instance is created by the buy coordinator and pushed onto the navigation stack of the coordinator's navigation controller.
private func buyPhoto(_ photo: Photo) {
// Initialize Buy Coordinator
let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)
// Start Buy Coordinator
buyCoordinator.start()
}
Let's revisit the showSignIn() method of the BuyCoordinator class. We create a SignInViewController instance, install the didSignIn and didCancel handlers, and push the sign in view controller onto the navigation stack of the coordinator's navigation controller.
The buy coordinator is weakly referenced in the didSignIn and didCancel handlers. This means that self is equal to nil in the handlers if the buy coordinator is deallocated. And that is exactly what happens.
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)
}
Open AppCoordinator.swift and navigate to the buyPhoto(_:) method. The BuyCoordinator instance is created and its start() method is invoked. As soon as the buyPhoto(_:) method returns, the BuyCoordinator instance is deallocated. That's what's breaking the purchase flow.
private func buyPhoto(_ photo: Photo) {
// Initialize Buy Coordinator
let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)
// Start Buy Coordinator
buyCoordinator.start()
}
The solution is simple. We need to keep a reference to the buy coordinator until the user completes or cancels the purchase flow. We have several options. We could define a property of type BuyCoordinator? to keep a reference to the BuyCoordinator instance. But that solution isn't scalable. We don't want to create a property for every child coordinator.
There's a better, more flexible option. The AppCoordinator class should manage an array of child coordinators. Create a private, variable property with name childCoordinators.
private var childCoordinators
What should the type of the childCoordinators property be? What type of objects should the array hold? We need to define a type that represents a child coordinator. We have two options. (1) We can create a base class from which every child coordinator inherits or (2) we can define a protocol a child coordinator needs to conform to. Let's take the protocol-oriented approach and define a protocol.
Defining a Protocol
Add a new Swift file to the Protocols group and name it Coordinator.swift. Define a protocol with name Coordinator. Because a coordinator should be a reference type, the Coordinator protocol inherits the AnyObject protocol.
protocol Coordinator: AnyObject {
}
Every coordinator needs to have a start() method to initiate the flow or subflow it manages. That's the only method the Coordinator protocol defines for now.
protocol Coordinator: AnyObject {
// MARK: - Methods
func start()
}
Revisit AppCoordinator.swift. Let's complete the definition of the childCoordinators property. The childCoordinators property is of type [Coordinator]. The default value of the childCoordinators property is an empty array.
import UIKit
import Foundation
class AppCoordinator {
// MARK: - Properties
private let navigationController = UINavigationController()
// MARK: -
private var childCoordinators: [Coordinator] = []
...
}
The BuyCoordinator instance is started in the buyPhoto(_:) method. Let's create a more generic method that accepts a coordinator, starts it, and appends it to the array of child coordinators. Create a method with name pushCoordinator(_:) that accepts an object that conforms to the Coordinator protocol.
private func pushCoordinator(_ coordinator: Coordinator) {
}
The implementation is straightforward. We invoke the start() method on the coordinator and append the coordinator to the array of child coordinators.
private func pushCoordinator(_ coordinator: Coordinator) {
// Start Coordinator
coordinator.start()
// Append to Child Coordinators
childCoordinators.append(coordinator)
}
Revisit the buyPhoto(_:) method. Instead of invoking the start() method of the BuyCoordinator instance, we invoke the pushCoordinator(_:) method, passing in the BuyCoordinator instance.
private func buyPhoto(_ photo: Photo) {
// Initialize Buy Coordinator
let buyCoordinator = BuyCoordinator(navigationController: navigationController, photo: photo)
// Push Buy Coordinator
pushCoordinator(buyCoordinator)
}
The compiler notifies us that the BuyCoordinator class doesn't conform to the Coordinator protocol. That's easy to fix. Open BuyCoordinator.swift and update the class definition by conforming the BuyCoordinator class to the Coordinator protocol.
import UIKit
class BuyCoordinator: Coordinator {
...
}
Build and run the application. Make sure the user is signed out. Buy a photo by tapping a photo in the table view and tapping the Buy button in the top right. Tap the Sign In button below the text fields to sign in. After signing in, the buy view controller is shown to the user. This means that we fixed the purchase flow. Tap the Buy button to buy the photo. Even though the purchase is successful, the navigation controller doesn't navigate back to the detail view of the photo. There still seems to be a loose end.
Finishing a Subflow
A child coordinator is responsible for a subflow of the application. A child coordinator needs to notify its parent coordinator when it has finished the subflow it is responsible for. That isn't difficult to implement.
Revisit the Coordinator protocol. When a child coordinator has finished the subflow it is responsible for, it should notify its parent coordinator by invoking a handler. Define a variable property with name didFinish. The property is an optional closure that accepts a Coordinator object as its only argument and returns Void. The property is gettable and settable.
protocol Coordinator: AnyObject {
// MARK: - Properties
var didFinish: ((Coordinator) -> Void)? { get set }
// MARK: - Methods
func start()
}
Open BuyCoordinator.swift and declare a property with name didFinish to conform the BuyCoordinator class to the updated Coordinator protocol.
import UIKit
class BuyCoordinator: Coordinator {
// MARK: - Properties
private let navigationController: UINavigationController
// MARK: -
private let photo: Photo
// MARK: -
var didFinish: ((Coordinator) -> Void)?
...
}
The buy coordinator needs to invoke the handler when it has finished the subflow it is responsible for. That is why we implemented the finish() method in the previous episode. This helper method is executed when the subflow of the coordinator is finished. In the finish() method, the coordinator executes the didFinish handler, passing in a reference to itself as the only argument.
// MARK: - Private API
private func finish() {
// Invoke Handler
didFinish?(self)
}
But there's more we need to handle in the finish() method. The application coordinator is unaware of the implementation of the child coordinators it manages. In other words, the application coordinator is unaware of the view controllers a child coordinator pushes onto the navigation stack of its navigation controller. It's the responsibility of the buy coordinator to reset the navigation controller to its original state.
When the purchase flow completes, the buy coordinator should navigate back to the detail view of the photo. The buy coordinator doesn't and shouldn't know about the photo view controller, but it can reset the navigation stack to its original state. We use a simple trick to accomplish that. Define a property, initialViewController, of type UIViewController?.
import UIKit
class BuyCoordinator: Coordinator {
// MARK: - Properties
private let navigationController: UINavigationController
// MARK: -
private let photo: Photo
// MARK: -
private let initialViewController: UIViewController?
// MARK: -
var didFinish: ((Coordinator) -> Void)?
...
}
The initialViewController property keeps a reference to the topmost view controller of the navigation stack before the buy coordinator starts the purchase flow. We ask the navigation controller for the last view controller of the array of view controllers in the initializer of the BuyCoordinator class.
// 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
}
In the finish() method, we safely unwrap the value of the initialViewController property. If initialViewController isn't equal to nil, then the buy coordinator navigates back to the initial view controller. If initialViewController is equal to nil, then the buy coordinator navigates back to the root view controller of the navigation controller.
// MARK: - Private API
private func finish() {
// Reset Navigation Controller
if let viewController = initialViewController {
navigationController.popToViewController(viewController, animated: true)
} else {
navigationController.popToRootViewController(animated: true)
}
// Invoke Handler
didFinish?(self)
}
Popping a Coordinator
With the didFinish handler in place, we can complete the implementation of the AppCoordinator class. Open AppCoordinator.swift. We install the didFinish handler of the Coordinator object in the pushCoordinator(_:) method. In the closure assigned to the didFinish handler, we invoke another helper method, popCoordinator(_:), passing in the Coordinator object.
private 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)
}
The implementation of the popCoordinator(_:) method is short. We ask the array of child coordinators for the index of the Coordinator object and use that index to remove the coordinator from the array of child coordinators. That's it.
private func popCoordinator(_ coordinator: Coordinator) {
// Remove Coordinator From Child Coordinators
if let index = childCoordinators.firstIndex(where: { $0 === coordinator }) {
childCoordinators.remove(at: index)
}
}
Build and run the application to make sure we restored the purchase flow. Tap a photo in the table view and tap the Buy button in the detail view of the photo. Sign in and tap the Buy button to buy the selected photo. After completing the purchase, the application navigates back to the detail view of the photo.
Click the Debug Memory Graph button in the debug bar at the bottom. Enter BuyCoordinator in the search field at the bottom. No instances of the BuyCoordinator class should be alive.

What's Next?
Notice that we refactored the AppCoordinator class and implemented the BuyCoordinator class without making any changes to the view controllers of the project. That's another benefit of the coordinator pattern and reusable, loosely coupled view controllers.