Navigation coordinators

· 5 minute read

Coordinators are an iOS design pattern that I’ve become a big fan of over the past few months. You should read Soroush’s post introducing them (and watch his conference talk as well), but in short, coordinators allow you to keep your view controllers isolated and reusable by preventing any one controller from knowing about any other. Each controller has its own delegate protocol, and a separate coordinator object acts as all of their delegates, performing all of the requisite routing. This makes it trivial to change the order in which view controllers are displayed, add support for new device classes, and so on and so forth.

One problem that I’ve been trying to work around is how coordinators are to be effectively used when a navigation controller is involved. Here’s an example in which we encapsulate an application’s signup flow inside of an “account creation coordinator.” You can envision a user tapping a Sign up button and being presented with a modal flow for creating an account. At the call site, that code would look like:

func signUpButtonTapped() {
  let accountCreationCoordinator = AccountCreationCoordinator(rootViewController:
      currentViewController, delegate: self)
  accountCreationCoordinator.start()

  /*
   We should assume that every coordinator may eventually maintain an array of
   child coordinators, which allows us to nest them and ensure that everything
   stays retained as long as it needs to be.
   */
  childCoordinators.append(accountCreationCoordinator)
}

The coordinator implementation would start out something like this:

final class AccountCreationCoordinator {
  // MARK: - Inputs

  private let rootViewController: UIViewController
  private weak var delegate: AccountCreationCoordinatorDelegate?

  // MARK: - Mutable state

  private let storage = Storage()

  private lazy var navigationController: UINavigationController = UINavigationController(
    UserNameAndPasswordViewController(delegate: self))

  // MARK: - Initialization

  init(rootViewController: UIViewController, delegate: AccountCreationCoordinatorDelegate) {
    self.rootViewController = rootViewController
  }

  // MARK: - Coordinator

  func start() {
    rootViewController.presentViewController(navigationController, animated: false)
  }
}

// MARK: - UserNameAndPasswordViewControllerDelegate

extension AccountCreationCoordinator: UserNameAndPasswordViewControllerDelegate {
  func userNameAndPasswordViewController(viewController: UserNameAndPasswordViewController,
      didSubmitCredentials credentials: Credentials) {
    storage.credentials = credentials

    // Next: Push a view controller for selecting an avatar
  }
}

It’d be easy enough to simply push an AvatarSelectionViewController onto the navigation controller, but let’s think about this a bit more. Avatar selection likely requires integrating with a UIImagePickerController, and perhaps even third-party APIs like Facebook or Twitter. To include this integration code inside of the AvatarSelectionViewController would be to go against the spirit of coordinators. We could put all of that code right here, inside of the AccountCreationCoordinator, but then that code wouldn’t be as reusable. Once they’ve registered, we probably want to allow our user to modify the avatar from somewhere inside of the application as well, so the best course of action here is to package all of this code up into an AvatarSelectionCoordinator, and have that coordinator be a child of our AccountCreationCoordinator.

extension AccountCreationCoordinator: UserNameAndPasswordViewControllerDelegate {
  func userNameAndPasswordViewController(viewController: UserNameAndPasswordViewController,
      didSubmitCredentials credentials: Credentials) {
    storage.credentials = credentials

    let avatarSelectionCoordinator = AvatarSelectionCoordinator(delegate: self)
    childCoordinators.append(avatarSelectionCoordinator)

    navigationController.pushViewController(avatarSelectionCoordinator.rootViewController,
      animated: true)
  }
}

(You’ll notice that I’ve called rootViewController on avatarSelectionCoordinator, instead of calling a start method. Exposing a read-only root view controller is useful for integrating coordinators in a variety of different places, navigation controllers being one but also when configuring your window’s rootViewController in your application delegate.)

The next step would be for our AccountCreationCoordinator to implement the AvatarSelectionCoordinatorDelegate protocol:

extension AccountCreationCoordinator: AvatarSelectionCoordinatorDelegate {
  func avatarSelectionCoordinator(coordinator: AvatarSelectionCoordinator,
      didSubmitImage image: UIImage) {
    storage.avatar = image

    childCoordinators.remove(coordinator)

    // Next: Push the next view controller in the flow
  }
}

Here’s the problem: while this works great so long as the user keeps moving forward through our onboarding flow, what would happen if they tapped the navigation controller’s back button? The AvatarSelectionCoordinator would still be retained by the childCoordinators array, and another AvatarSelectionCoordinator would end up being added to the same array if the user was to submit the username and password form again. Not good.

One solution would be to have AccountCreationCoordinator monitor the navigation controller’s view controller stack, and ensure that a coordinator is cleaned up if its root view controller is no longer included. This would involve:

  • Having AccountCreationCoordinator conform to UINavigationControllerDelegate
  • Having AccountCreationCoordinator maintain a mapping between root view controllers and their respective coordinators

This is fairly straightforward, but this isn’t account creation-specific behavior. We’ll want this same behavior everywhere in our application where coordinators might come into contact with navigation controllers.

When trying to come up with a more elegant solution, I found myself drawing inspiration from a quote from Soroush’s original coordinator doctrine:

You’re not sitting around waiting for -viewDidLoad to get called so you can do work, you’re totally in control of the show. There’s no invisible code in a UIViewController superclass that is doing some magic that you don’t understand. Instead of being called, you start doing the calling.

Flipping this model makes it much easier to understand what’s going on. The behavior of your app is a completely transparent to you, and UIKit is now just a library that you call when you want to use it.

Instead of having our account coordinator be called, let’s see if we can’t instead call something that treats UINavigationController as a library.

protocol RootViewControllerProvider: class {
  var rootViewController: UIViewController { get }
}

typealias RootCoordinator = protocol<Coordinator, RootViewControllerProvider>

final class NavigationController: UIViewController {
  // MARK: - Inputs

  private let rootViewController: UIViewController

  // MARK: - Mutable state

  private var viewControllersToChildCoordinators: [UIViewController: Coordinator] = [:]

  // MARK: - Lazy views

  private lazy var childNavigationController: UINavigationController =
      UINavigationController(rootViewController: self.rootViewController)

  // MARK: - Initialization

  init(rootViewController: UIViewController) {
    self.rootViewController = rootViewController

    super.init(nibName: nil, bundle: nil)
  }

  // MARK: - UIViewController

  override func viewDidLoad() {
    super.viewDidLoad()

    childNavigationController.delegate = self
    childNavigationController.interactivePopGestureRecognizer?.delegate = self

    addChildViewController(childNavigationController)
    view.addSubview(childNavigationController.view)
    childNavigationController.didMoveToParentViewController(self)

    childNavigationController.view.translatesAutoresizingMaskIntoConstraints = false

    NSLayoutConstraint.activateConstraints([
      childNavigationController.view.topAnchor.constraintEqualToAnchor(view.topAnchor),
      childNavigationController.view.leftAnchor.constraintEqualToAnchor(view.leftAnchor),
      childNavigationController.view.rightAnchor.constraintEqualToAnchor(view.rightAnchor),
      childNavigationController.view.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
    ])
  }

  // MARK: - Public

  func pushCoordinator(coordinator: RootCoordinator, animated: Bool) {
    viewControllersToChildCoordinators[coordinator.rootViewController] = coordinator

    pushViewController(coordinator.rootViewController, animated: animated)
  }

  func pushViewController(viewController: UIViewController, animated: Bool) {
    childNavigationController.pushViewController(viewController, animated: animated)
  }
}

// MARK: - UIGestureRecognizerDelegate

extension NavigationController: UIGestureRecognizerDelegate {
  func gestureRecognizer(gestureRecognizer: UIGestureRecognizer,
      shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    // Necessary to get the child navigation controller’s interactive pop gesture recognizer to work.
    return true
  }
}

// MARK: - UINavigationControllerDelegate

extension NavigationController: UINavigationControllerDelegate {    
  func navigationController(navigationController: UINavigationController,
      didShowViewController viewController: UIViewController, animated: Bool) {
    cleanUpChildCoordinators()
  }

  // MARK: - Private

  private func cleanUpChildCoordinators() {
    for viewController in viewControllersToChildCoordinators.keys {
      if !childNavigationController.viewControllers.contains(viewController) {
          viewControllersToChildCoordinators.removeValueForKey(viewController)
      }
    }
  }
}

This NavigationController class can be used in conjunction with coordinators throughout our application, making it easy for us to compose coordinators inside one another for maximum reusability.

UIViewController composition is a really powerful pattern that I’ve been using to great effect in a new project I’ve been working on. Yes, it inherently requires more boilerplate plumbing. For example, my NavigationController above only exposes the ability to push a new view controller onto the navigation stack; none of the other public methods or properties that classes interacting directly with a UINavigationController instance would be able to call are available. I think this is a worthwhile tradeoff, however. It’s not a huge deal to proxy some method calls through if I want my custom class to expose more of UINavigationController’s capabilities, and in the meantime, I’m benefiting from exposing a minimal surface area with very little mutability. If I ever want to do something more elaborate, like implement my own replacement for UINavigationController, I wouldn’t need to make code changes at any of NavigationController’s call sites.

While being diligent about using coordinators to route between view controllers has been a valuable exercise, there have been cases like this one where it would’ve been easy to succumb to how UIKit tends to imply ones code should be structured. The original thinking behind coordinators was that your application’s flow should be modeled by your own objects, with `UIKit` components serving as an implementation detail wherever possible. It shouldn’t be too surprising that going back to that original mantra led to exactly the solution that I needed in this instance. I presume that’ll continue to be the case in the future.