Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

23. Coordinator Pattern
Written by Joshua Greene

The coordinator pattern is a structural design pattern for organizing flow logic between view controllers. It involves the following components:

  1. The coordinator is a protocol that defines the methods and properties all concrete coordinators must implement. Specifically, it defines relationship properties, children and router. It also defines presentation methods, present and dismiss.

    By holding onto coordinator protocols, instead of onto concrete coordinators directly, you can decouple a parent coordinator and its child coordinators. This enables a parent coordinator to hold onto various concrete child coordinators in a single property, children.

    Likewise, by holding onto a router protocol instead of a concrete router directly, you can decouple the coordinator and its router.

  2. The concrete coordinator implements the coordinator protocol. It knows how to create concrete view controllers and the order in which view controllers should be displayed.

  3. The router is a protocol that defines methods all concrete routers must implement. Specifically, it defines present and dismiss methods for showing and dismissing view controllers.

  4. The concrete router knows how to present view controllers, but it doesn’t know exactly what is being presented or which view controller will be presented next. Instead, the coordinator tells the router which view controller to present.

  5. The concrete view controllers are typical UIViewController subclasses found in MVC. However, they don’t know about other view controllers. Instead, they delegate to the coordinator whenever a transition needs to performed.

This pattern can be adopted for only part of an app, or it can be used as an “architectural pattern” to define the structure of an entire app.

You’ll see both of these at work in this chapter: In the Playground example, you’ll call a coordinator from an existing view controller, and in the Tutorial Project, you’ll adopt this pattern across the entire app.

When should you use it?

Use this pattern to decouple view controllers from one another. The only component that knows about view controllers directly is the coordinator.

Consequently, view controllers are much more reusable: If you want to create a new flow within your app, you simply create a new coordinator!

Playground example

Open AdvancedDesignPatterns.xcworkspace in the starter directory, and then open the Coordinator page.

For this playground example, you’ll create a step-by-step instruction flow. You could use this for any instructions, such as app set up, first-time-help tutorials or any other step-by-step flow.

To keep the example simple and focused on the design pattern, you’ll create a “How to Code” flow that will show a set of view controllers with text only.

Hold down Option and left-click the arrow next to the Coordinator page to expand all of its subfolders. You’ll see several folders have already been added for you:

Controllers contains all of the concrete view controllers. These are simple, vanilla view controllers and have already been implemented for you.

Coordinators contains two files: Coordinator.swift and HowToCodeCoordinator.swift. If you open each, you’ll see they are currently empty.

Likewise, Routers contains two files: NavigationRouter.swift and Router.swift. Both of which are also currently empty.

These types are what you need to implement!

Creating the Router Protocol

First, open Router.swift. This is where you’ll implement the Router protocol.

Add the following code to this file:

import UIKit

public protocol Router: class {
  // 1
  func present(_ viewController: UIViewController, 
                animated: Bool)
                
  func present(_ viewController: UIViewController,
               animated: Bool,
               onDismissed: (()->Void)?)
  // 2
  func dismiss(animated: Bool)
}

extension Router {
  // 3
  public func present(_ viewController: UIViewController,
                      animated: Bool) {
    present(viewController, 
            animated: animated, 
            onDismissed: nil)
  }
}

You’re declaring a protocol called Router here. Here’s what this protocol defines:

  1. You first define two present methods. The only difference is one takes an onDismissed closure, and the other doesn’t. If provided, concrete routers will execute the onDismissed whenever a view controller is dismissed, for example via a “pop“ action in the case of a concrete router that uses a UINavigationController.

  2. You also declare dismiss(animated:). This will dismiss the entire router. Depending on the concrete router, this may result in popping to a root view controller, calling dismiss on a parentViewController or whatever action is necessary per the concrete router’s implementation.

  3. You lastly define a default implementation for present(_:animated:). This simply calls the other present by passing nil for onDismissed.

You may be wondering, “Don’t I need a method to dismiss individual view controllers?” Surprisingly, you may not need one! Neither this playground example nor the tutorial project require it. If you actually do need this in your own project, feel free to declare one!

Creating the Concrete Router

You next need to implement the Concrete Router. Open NavigationRouter.swift, and add the following code to it:

import UIKit

// 1
public class NavigationRouter: NSObject {

  // 2
  private let navigationController: UINavigationController
  private let routerRootController: UIViewController?
  private var onDismissForViewController:
    [UIViewController: (() -> Void)] = [:]

  // 3
  public init(navigationController: UINavigationController) {
    self.navigationController = navigationController
    self.routerRootController =
      navigationController.viewControllers.first
    super.init()
  }
}

Let’s go over this:

  1. You declare NavigationRouter as a subclass of NSObject. This is required because you’ll later make this conform to UINavigationControllerDelegate.

  2. You then create these instance properties:

  • navigationController will be used to push and pop view controllers.
  • routerRootController will be set to the last view controller on the navigationController. You’ll use this later to dismiss the router by popping to this.
  • onDismissForViewController is a mapping from UIViewController to on-dismiss closures. You’ll use this later to perform an on-dismiss actions whenever view controllers are popped.
  1. You lastly create an initializer that takes a navigationController, and you set the navigationController and routerRootController from it.

You’ll notice that this doesn’t implement the Router protocol yet. So let’s do that! Add the following extension to the end of the file:

// MARK: - Router
extension NavigationRouter: Router {

  // 1
  public func present(_ viewController: UIViewController,
                      animated: Bool,
                      onDismissed: (() -> Void)?) {
    onDismissForViewController[viewController] = onDismissed
    navigationController.pushViewController(viewController,
                                            animated: animated)
  }

  // 2
  public func dismiss(animated: Bool) {
    guard let routerRootController = routerRootController else {
      navigationController.popToRootViewController(
        animated: animated)
      return
    }
    performOnDismissed(for: routerRootController)
    navigationController.popToViewController(
      routerRootController,
      animated: animated)
  }

  // 3
  private func performOnDismissed(for
    viewController: UIViewController) {

    guard let onDismiss =
      onDismissForViewController[viewController] else {
      return
    }
    onDismiss()
    onDismissForViewController[viewController] = nil
  }
}

This makes NavigationRouter conform to Router:

  1. Within present(_:animated:onDismissed:), you set the onDismissed closure for the given viewController and then push the view controller onto the navigationController to show it.

  2. Within dismiss(animated:), you verify that routerRootController is set. If not, you simply call popToRootViewController(animated:) on the navigationController. Otherwise, you call performOnDismissed(for:) to perform the on-dismiss action and then pass the routerRootController into popToViewController(_:animated:) on the navigationController.

  3. Within performOnDismiss(for:), you guard that there’s an onDismiss for the given viewController. If not, you simply return early. Otherwise, you call onDismiss and remove it from onDismissForViewController.

The last thing you need to do here is make NavigationRouter conform to UINavigationController, so you can call the on-dismiss action if the user presses the back button. Add the following extension to the end of the file:

// MARK: - UINavigationControllerDelegate
extension NavigationRouter: UINavigationControllerDelegate {

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

    guard let dismissedViewController =
      navigationController.transitionCoordinator?
        .viewController(forKey: .from),
      !navigationController.viewControllers
        .contains(dismissedViewController) else {
      return
    }
    performOnDismissed(for: dismissedViewController)
  }
}

Inside navigationController(_:didShow:animated:), you get the from view controller from the navigationController.transitionCoordinator and verify it’s not contained within navigationController.viewControllers. This indicates that the view controller was popped, and in response, you call performOnDismissed to do the on-dismiss action for the given view controller.

Of course, you also need to actually set NavigationRouter as the delegate for the navigationController.

Add the following to the end of init(navigationController:):

navigationController.delegate = self

With this, your NavigationRouter is complete!

Creating the Coordinator

Your next task is to create the Coordinator protocol. Open Coordinator.swift and add the following to it:

public protocol Coordinator: class {

  // 1
  var children: [Coordinator] { get set }
  var router: Router { get }

  // 2
  func present(animated: Bool, onDismissed: (() -> Void)?)
  func dismiss(animated: Bool)
  func presentChild(_ child: Coordinator,
                    animated: Bool,
                    onDismissed: (() -> Void)?)
}

Here’s what this does:

  1. You declare relationship properties for children and router. You’ll use these properties to provide default implementations within an extension on Coordinator next.

  2. You also declare required methods for present, dismiss and presentChild.

You can provide reasonable default implementations for both dismiss and presentChild. Add the following extension to the end of the file:

extension Coordinator {

  // 1
  public func dismiss(animated: Bool) {
    router.dismiss(animated: true)
  }

  // 2
  public func presentChild(_ child: Coordinator,
                           animated: Bool,
                           onDismissed: (() -> Void)? = nil) {
    children.append(child)
    child.present(
      animated: animated,
      onDismissed: { [weak self, weak child] in
        guard let self = self,
          let child = child else {
            return
        }
        self.removeChild(child)
        onDismissed?()
    })
  }
  
  private func removeChild(_ child: Coordinator) {
    guard let index = children.firstIndex(
      where: { $0 === child }) else {
        return
    }
    children.remove(at: index)
  }
}

Here’s what this does:

  1. To dismiss a coordinator, you simply call dismiss on its router. This works because whoever presented the coordinator is responsible for passing an onDismiss closure to do any required teardown, which will be called by the router automatically.

    Remember how you wrote all that logic within NavigationRouter for handling popping and dismissing? This is why you did that!

  2. Within presentChild, you simply append the given child to children, and then call child.present. You also take care of removing the child by calling removeChild(_:) within the child’s onDismissed action, and lastly, you call the provided onDismissed passed into the method itself.

Just like the Router didn’t declare a dismiss method for individual view controllers, this Coordinator doesn’t declare a dismiss method for child coordinators. The reasoning is the same: the examples in this chapter don’t require it! Of course, feel free to add them, if necessary, to your application.

Creating the concrete coordinator

The last type you need to create is the Concrete Coordinator. Open HowToCodeCoordinator.swift and add the following code, ignoring any compiler errors you get for now:

import UIKit

public class HowToCodeCoordinator: Coordinator {

  // MARK: - Instance Properties
  // 1
  public var children: [Coordinator] = []
  public let router: Router

  // 2
  private lazy var stepViewControllers = [
    StepViewController.instantiate(
      delegate: self,
      buttonColor: UIColor(red: 0.96, green: 0, blue: 0.11,
                           alpha: 1),
      text: "When I wake up, well, I'm sure I'm gonna be\n\n" +
      "I'm gonna be the one writin' code for you",
      title: "I wake up"),

    StepViewController.instantiate(
      delegate: self,
      buttonColor: UIColor(red: 0.93, green: 0.51, blue: 0.07,
                           alpha: 1),
      text: "When I go out, well, I'm sure I'm gonna be\n\n" +
      "I'm gonna be the one thinkin' bout code for you",
      title: "I go out"),

    StepViewController.instantiate(
      delegate: self,
      buttonColor: UIColor(red: 0.23, green: 0.72, blue: 0.11,
                           alpha: 1),
      text: "Cause' I would code five hundred lines\n\n" +
      "And I would code five hundred more",
      title: "500 lines"),

    StepViewController.instantiate(
      delegate: self,
      buttonColor: UIColor(red: 0.18, green: 0.29, blue: 0.80,
                           alpha: 1),
      text: "To be the one that wrote a thousand lines\n\n" +
      "To get this code shipped out the door!",
      title: "Ship it!")
  ]

  // 3
  private lazy var startOverViewController =
    StartOverViewController.instantiate(delegate: self)

  // MARK: - Object Lifecycle
  // 4
  public init(router: Router) {
    self.router = router
  }

  // MARK: - Coordinator
  // 5
  public func present(animated: Bool, 
                      onDismissed: (() -> Void)?) {
    let viewController = stepViewControllers.first!
    router.present(viewController,
                   animated: animated,
                   onDismissed: onDismissed)
  }
}

Here’s what you’ve done:

  1. First, you declare properties for children and router, which are required to conform to Coordinator and Router respectively.

  2. Next, you create an array called stepViewControllers, which you set by instantiating several StepViewController objects. This is a simple view controller that displays a button with a multiline label.

    You set the view controllers’ texts to parody song lyrics of “I’m Gonna Be (500 miles)” by the Proclaimers. Google it if you don’t know it. Be sure to sing these lyrics aloud to this tune, especially if others are nearby — they’ll love it..! Well, depending on your singing skill, maybe it’s best if you sing alone!

  3. Next, you declare a property for startOverViewController. This will be the last view controller displayed and will simply show a button to “start over.”

  4. Next, you create a designated initializer that accepts and sets the router.

  5. Finally, you implement present(animated:, onDismissed:), which is required by Coordinator to start the flow.

You next need to make HowToCodeCoordinator conform to StepViewControllerDelegate. Add the following code to the end of the file; continue ignoring the other compiler errors for now:

// MARK: - StepViewControllerDelegate
extension HowToCodeCoordinator: StepViewControllerDelegate {
  
  public func stepViewControllerDidPressNext(
    _ controller: StepViewController) {
    if let viewController =
      stepViewController(after: controller) {
      router.present(viewController, animated: true)
    } else {
      router.present(startOverViewController, animated: true)
    }
  }
  
  private func stepViewController(after
    controller: StepViewController) -> StepViewController? {
    guard let index = stepViewControllers
      .firstIndex(where: { $0 === controller }),
      index < stepViewControllers.count - 1 else { return nil }
    return stepViewControllers[index + 1]
  }
}

Within stepViewControllerDidPressNext(_:), you first attempt to get the next StepViewController, which is returned by stepViewController(after:) as long as this isn’t the last one. You then pass this to router.present(_:animated:) to show it.

If there isn’t a next StepViewController, you pass startOverViewController to router.present(_:animated:) instead.

To resolve the remaining compiler errors, you need to make HowToCodeCoordinator conform to StartOverViewControllerDelegate. Add the following code to the end of the file to do so:

// MARK: - StartOverViewControllerDelegate
extension HowToCodeCoordinator:
  StartOverViewControllerDelegate {

  public func startOverViewControllerDidPressStartOver(
    _ controller: StartOverViewController) {
    router.dismiss(animated: true)
  }
}

Whenever startOverViewControllerDidPressStartOver(_:) is called, you call router.dismiss to end the flow. Ultimately, this will result in returning to the first view controller that initiated the flow, and hence, the user can start it again.

Trying out the playground example

You’ve created all of the components, and you’re ready to put them into action!

Open the Coordinator page, and add the following right below Code Example:

import PlaygroundSupport
import UIKit

// 1
let homeViewController = HomeViewController.instantiate()
let navigationController = UINavigationController(rootViewController: homeViewController)

// 2
let router = NavigationRouter(navigationController: navigationController)
let coordinator = HowToCodeCoordinator(router: router)

// 3
homeViewController.onButtonPressed = { [weak coordinator] in
  coordinator?.present(animated: true, onDismissed: nil)
}

// 4
PlaygroundPage.current.liveView = navigationController

Let’s go over this:

  1. First, you create homeViewController, and then use this to create navigationController. This will be the “home” screen. If this were actually an iOS app instead, this would be the first screen shown whenever the app is launched.

  2. Next, you create the router using the navigationController, and in turn, create the coordinator using the router.

  3. If you open HomeViewController.swift, you’ll see it has a single button that ultimately calls its onButtonPressed closure. Here, you set homeViewController.onButtonPressed to tell the coordinator to present, which will start its flow.

  4. Finally, you set the PlaygroundPage.current.liveView to the navigationController, which tells Xcode to display the navigationController within the assistant editor.

Run the playground, and you should see the Live preview showing this in action. If you don’t, select Editor and ensure Live View is checked.

Tap on How to Code to start the flow. Tap each of the buttons until you get to Start Over. Once you tap this, the coordinator will be dismissed, and you’ll see How to Code again.

What should you be careful about?

Make sure you handle going-back functionality when using this pattern. Specifically, make sure you provide any required teardown code passed into onDismiss on the coordinator’s present(animated:onDismiss:).

For very simple apps, the Coordinator pattern may seem like overkill. You’ll be required to create many additional classes upfront; namely, the concrete coordinator and routers.

For long-term or complex apps, the coordinator pattern can help you provide needed structure and increase view controllers’ reusability.

Tutorial project

You’ll build an app called RayPets in this chapter. This is a “pet” project by Ray: an exclusive pets-only clinic for savvy iOS users.

Open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter ▸ RayPets ▸ RayPets.xcodeproj in Xcode.

Build and run, and you’ll see this home screen:

If you tap on Schedule Visit, however, nothing happens!

Before investigating why, take a look at the file hierarchy. In particular, you’ll find there’s a Screens group, which contains view controllers and views that have been implemented already.

These are your typical view controllers found in MVC. However, per the coordinator pattern, they don’t know about view controller transitions. Instead, each informs its delegate whenever a transition is required.

Lastly, take a look at Screens ▸ Protocols ▸ StoryboardInstantiable.swift. Each view controller within RayPets conforms to this protocol. It makes instantiating a view controller from a storyboard easier. In particular, it provides a static method called instanceFromStoryboard that returns Self to create a view controller from its storyboard.

Next, go to Screens ▸ Home ▸ Controllers ▸ HomeViewController.swift. You’ll see an IBAction for didPressScheduleAppointment, which is called in response to tapping Schedule Visit. This in turn calls homeViewControllerDidPressScheduleAppointment on its delegate.

However, this part of the app hasn’t been implemented yet. To do so, you need to implement a new concrete coordinator and router.

In the file hierarchy, you’ll also see there’s already a group for Coordinators. Within this, you’ll find Coordinator.swift has already been copied from the playground example.

You’ll also see a Routers group. This contains Router.swift that has likewise been copied from the playground example.

Creating AppDelegateRouter

You’ll first implement a new concrete router. Within the Routers group, create a new file called AppDelegateRouter.swift, and replace its contents with the following:

import UIKit

public class AppDelegateRouter: Router {

  // MARK: - Instance Properties
  public let window: UIWindow

  // MARK: - Object Lifecycle
  public init(window: UIWindow) {
    self.window = window
  }

  // MARK: - Router
  public func present(_ viewController: UIViewController,
                      animated: Bool,
                      onDismissed: (()->Void)?) {
    window.rootViewController = viewController
    window.makeKeyAndVisible()
  }

  public func dismiss(animated: Bool) {
    // don't do anything
  }
}

This router is intended to hold onto the window from the AppDelegate.

Within present(_:animated:onDismissed:), you simply set the window.rootViewController and call window.makeKeyAndVisible to show the window.

This router will be held onto by the AppDelegate directly and isn’t meant to be dismissible. Thereby, you simply ignore calls to dismiss(animated:).

Creating HomeCoordinator

You next need to create a coordinator to instantiate and display the HomeViewController. Within the Coordinators group, create a new file named HomeCoordinator.swift. Replace its contents with the following, ignoring the compiler error for now:

import UIKit

public class HomeCoordinator: Coordinator {

  // MARK: - Instance Properties
  public var children: [Coordinator] = []
  public let router: Router

  // MARK: - Object Lifecycle
  public init(router: Router) {
    self.router = router
  }

  // MARK: - Instance Methods
  public func present(animated: Bool,
                      onDismissed: (() -> Void)?) {
    let viewController = 
      HomeViewController.instantiate(delegate: self)
    router.present(viewController,
                   animated: animated,
                   onDismissed: onDismissed)
  }
}

This coordinator is pretty simple. You create properties for children and router, which are required by the Coordinator protocol, and a simple initializer that sets the router.

Within present(animated:onDismissed:), you instantiate HomeViewController by calling a convenience constructor method, instantiate(delegate:). You then pass this into router.present(_:animated:onDismissed:) to show it. To resolve the compiler error, you need to make HomeCoordinator conform to HomeViewControllerDelegate. Add the following extension to the end of the file:

// MARK: - HomeViewControllerDelegate
extension HomeCoordinator: HomeViewControllerDelegate {

  public func homeViewControllerDidPressScheduleAppointment(
    _ viewController: HomeViewController) {
    // TODO: - Write this
  }
}

You’ve simply stubbed out this method for now.

Using HomeCoordinator

You also need to actually use HomeCoordinator. To do so, open AppDelegate.Swift and replace its contents with the following:

import UIKit

@UIApplicationMain
public class AppDelegate: UIResponder, UIApplicationDelegate {

  // MARK: - Instance Properties
  // 1
  public lazy var coordinator = HomeCoordinator(router: router)
  public lazy var router = AppDelegateRouter(window: window!)
  public lazy var window: UIWindow? =
    UIWindow(frame: UIScreen.main.bounds)

  // MARK: - Application Lifecycle
  // 2
  public func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
    -> Bool {
    coordinator.present(animated: true, onDismissed: nil)
    return true
  }
}

Here’s what you’ve done:

  1. You first create lazy properties for coordinator, router, and window.

  2. Then within application(_:didFinishLaunchingWithOptions:), you call coordinator.present to start the HomeCoordinator flow.

Build and run, and you’ll see the application displays the HomeViewController, just at did before. However, you’re now set up to implement the coordinator pattern across the entire app!

In particular, you’ll next focus on implementing a new coordinator for scheduling a pet appointment, in response to pressing Schedule Visit.

Creating PetAppointmentBuilderCoordinator

Open Models ▸ PetAppointment.swift, and you’ll see a model and related builder has already been defined: PetAppointment and PetAppointmentBuilder.

You’ll create a new coordinator for the purpose of collecting PetAppointmentBuilder inputs from the user. Create a new file called PetAppointmentBuilderCoordinator.swift in the Coordinators group, and replace its contents with the following, ignoring the compiler error for now:

import UIKit

public class PetAppointmentBuilderCoordinator: Coordinator {

  // MARK: - Instance Properties
  public let builder = PetAppointmentBuilder()
  public var children: [Coordinator] = []
  public let router: Router

  // MARK: - Object Lifecycle
  public init(router: Router) {
    self.router = router
  }

  // MARK: - Instance Methods
  public func present(animated: Bool,
                      onDismissed: (() -> Void)?) {
    let viewController =
      SelectVisitTypeViewController.instantiate(delegate: self)
    router.present(viewController,
                   animated: animated,
                   onDismissed: onDismissed)
  }
}

PetAppointmentBuilderCoordinator has a property for builder, which you’ll use to set inputs from the user, and required properties for children and router, per the Coordinator protocol.

Within present(animated:onDismissed:), you instantiate a SelectVisitTypeViewController via instantiate(delegate:) and then pass this to router.present(_:animated:onDismissed).

Sounds familiar, right? This is very similar to HomeCoordinator, and it’s a recurring pattern you’ll see using coordinators: you instantiate a view controller, pass it to the router to present it and receive feedback via delegate callbacks.

Thereby, you need to make PetAppointmentBuilderCoordinator conform to SelectVisitTypeViewControllerDelegate. Add the following extension to the end of the file, again ignoring compiler errors for now:

// MARK: - SelectVisitTypeViewControllerDelegate
extension PetAppointmentBuilderCoordinator:
  SelectVisitTypeViewControllerDelegate {

  public func selectVisitTypeViewController(
    _ controller: SelectVisitTypeViewController,
    didSelect visitType: VisitType) {

    // 1
    builder.visitType = visitType
    
    // 2
    switch visitType {
    case .well:
      // 3
      presentNoAppointmentViewController()
    case .sick:
      // 4
      presentSelectPainLevelCoordinator()
    }
  }

  private func presentNoAppointmentViewController() {
    let viewController =
      NoAppointmentRequiredViewController.instantiate(
        delegate: self)
    router.present(viewController, animated: true)
  }

  private func presentSelectPainLevelCoordinator() {
    let viewController =
      SelectPainLevelViewController.instantiate(delegate: self)
    router.present(viewController, animated: true)
  }
}

Here’s what this does:

  1. Within selectVisitTypeViewController(_:didSelect:), you first set builder.visitType.

  2. You then switch on the selected visitType.

  3. If the visitType is well, you call presentNoAppointmentViewController() to show a NoAppointmentRequiredViewController.

  4. If it is sick, you call presentSelectPainLevelCoordinator() to show a SelectPainLevelViewController.

Both NoAppointmentRequiredViewController and SelectPainLevelViewController each require their own delegate, but PetAppointmentBuilderCoordinator doesn’t conform to their delegate protocols yet.

Next, add the following to the end of the file to make PetAppointmentBuilderCoordinator conform to SelectPainLevelViewControllerDelegate:

// MARK: - SelectPainLevelViewControllerDelegate
extension PetAppointmentBuilderCoordinator:
  SelectPainLevelViewControllerDelegate {

  public func selectPainLevelViewController(
    _ controller: SelectPainLevelViewController,
    didSelect painLevel: PainLevel) {

    // 1
    builder.painLevel = painLevel

    // 2
    switch painLevel {

    // 3
    case .none, .little:
      presentFakingItViewController()

    // 4
    case .moderate, .severe, .worstPossible:
      presentNoAppointmentViewController()
    }
  }

  private func presentFakingItViewController() {
    let viewController =
      FakingItViewController.instantiate(delegate: self)
    router.present(viewController, animated: true)
  }
}

This parallels how you handled the previous delegate interaction:

  1. Within selectPainLevelViewController(_:didSelect:), you first set the builder.painLevel.

  2. You then switch on the selected painLevel.

  3. If the case matches none or little, you call presentFakingItViewController() to show a FakingItViewController.

  4. If the case matches moderate, severe or worstPossible, you can presentNoAppointmentViewController() to show a NoAppointmentRequiredViewController.

As a consequence, you need to implement yet another delegate protocol: FakingItViewControllerDelegate. Fortunately, this one is pretty easy. Add the following code to the end of the file:

// MARK: - FakingItViewControllerDelegate
extension PetAppointmentBuilderCoordinator:
  FakingItViewControllerDelegate {

  public func fakingItViewControllerPressedIsFake(
    _ controller: FakingItViewController) {
    router.dismiss(animated: true)
  }

  public func fakingItViewControllerPressedNotFake(
    _ controller: FakingItViewController) {
    presentNoAppointmentViewController()
  }
}

This interaction is pretty straightforward:

  1. Within fakingItViewControllerPressedIsFake(_:), you simply call router.dismiss(animated:) to exit out of the coordinator flow.

  2. Within fakingItViewControllerPressedNotFake(_:), you again call presentNoAppointmentViewController() to show a NoAppointmentRequiredViewController.

Wait a minute — so no matter what the user does, they ultimately winds up seeing NoAppointmentRequiredViewController. What’s the deal with that?

I talked with Ray about this, and uh, he says it’s a marketing tactic to get customers to come into the office… either that or, someone didn’t write a backend for this example app. So it appears there’s nowhere to actually submit this data. What are you going to do now?

Not to fear! Just like in real life when the backend isn’t ready, you fake it! Hence, the app ultimately shows the NoAppointmentRequiredViewController, regardless of the prior selection.

You have just one more protocol you need to implement: NoAppointmentRequiredViewControllerDelegate. Add this code to the end of this file:

// MARK: - NoAppointmentRequiredViewControllerDelegate
extension PetAppointmentBuilderCoordinator:
  NoAppointmentRequiredViewControllerDelegate {

  public func noAppointmentViewControllerDidPressOkay(
    _ controller: NoAppointmentRequiredViewController) {
    router.dismiss(animated: true)
  }
}

In response to noAppointmentViewControllerDidPressOkay(_:), you simply call router.dismiss(animated:) to exit the app flow.

Great! You now just need a concrete router to use with this coordinator.

Creating ModalNavigationRouter

You may be wondering, “Couldn’t I just use NavigationRouter from the playground example?”

NavigationRouter requires an existing UINavigationController and pushing view controllers onto it. Since the schedule-a-visit flow is distinct from the Home flow, Ray really wants this to be presented modally, which NavigationRouter isn’t designed to do.

Instead, you’ll create a new router that creates a new UINavigationController and presents it using an existing parentViewController to support this use case.

Within the Routers group, create a new file called ModalNavigationRouter.swift, and replace its contents with the following:

import UIKit

// 1
public class ModalNavigationRouter: NSObject {

  // MARK: - Instance Properties
  // 2
  public unowned let parentViewController: UIViewController

  private let navigationController = UINavigationController()
  private var onDismissForViewController:
    [UIViewController: (() -> Void)] = [:]

  // MARK: - Object Lifecycle
  // 3
  public init(parentViewController: UIViewController) {
    self.parentViewController = parentViewController
    super.init()
  }
}

Here’s what you’ve done:

  1. First, you declare ModalNavigationRouter as a subclass of NSObject. Being a subclass of NSObject is required because you’ll later make this conform to UINavigationControllerDelegate.

  2. Next, you create instance properties for parentViewController, navigationController and onDismissForViewController.

  3. Finally, you declare an initializer that accepts the parentViewController.

Next, you need to make ModalNavigationRouter conform to Router. Add the following code to the bottom of the file:

// MARK: - Router
extension ModalNavigationRouter: Router {

  // 1
  public func present(_ viewController: UIViewController,
                      animated: Bool,
                      onDismissed: (() -> Void)?) {
    onDismissForViewController[viewController] = onDismissed
    if navigationController.viewControllers.count == 0 {
      presentModally(viewController, animated: animated)
    } else {
      navigationController.pushViewController(
        viewController, animated: animated)
    }
  }

  private func presentModally(
    _ viewController: UIViewController,
    animated: Bool) {
    // 2
    addCancelButton(to: viewController)

    // 3
    navigationController.setViewControllers(
      [viewController], animated: false)
    parentViewController.present(navigationController,
                                 animated: animated,
                                 completion: nil)
  }

  private func addCancelButton(to
    viewController: UIViewController) {
    viewController.navigationItem.leftBarButtonItem =
    UIBarButtonItem(title: "Cancel",
                    style: .plain,
                    target: self,
                    action: #selector(cancelPressed))
  }

  @objc private func cancelPressed() {
    performOnDismissed(for:
      navigationController.viewControllers.first!)
    dismiss(animated: true)
  }

  // 4
  public func dismiss(animated: Bool) {
    performOnDismissed(for:
      navigationController.viewControllers.first!)
    parentViewController.dismiss(animated: animated,
                                 completion: nil)
  }

  // 5
  private func performOnDismissed(for 
    viewController: UIViewController) {
    guard let onDismiss = 
      onDismissForViewController[viewController] else { return }
    onDismiss()
    onDismissForViewController[viewController] = nil
  }
}

Here’s what this does:

  1. In present(_:animated:onDismissed:), you first set onDismissForViewController for the given view controller to the given closure. You then check if the navigationController doesn’t have any view controllers. This means that you need to modally present the navigation controller, which you do by calling presentModally(_:animated:). Otherwise, you call pushViewController on the navigationController.

  2. Within presentModally(_:animated:), you first pass the view controller to addCancelButton(to:) in order to set up a Cancel button on the view controller. If the button is tapped, it will call cancelPressed(), perform the on-dismiss action and ultimately call dismiss(animated:).

  3. Next, still within presentModally(_:animated:), you set the viewControllers on the navigationController using the passed-in viewController. You then call present on the parentViewController in order to modally present the navigationController.

  4. Within dismiss(animated:), you call performOnDismissed passing navigationController.viewControllers.first. You then tell the parentViewController to dismiss its presented view controller, which is the navigationController.

  5. performOnDismissed(for:) is exactly the same as the method in NavigationRouter: It checks if there’s an existing onDismiss closure for the view controller, executes it if found, and finally removes the closure from onDismissForViewController.

Great! The only remaining task to complete ModalNavigationRouter is to make it conform to UINavigationControllerDelegate. Add the following to the end of the file for this:

// MARK: - UINavigationControllerDelegate
extension ModalNavigationRouter: 
  UINavigationControllerDelegate {

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

    guard let dismissedViewController =
      navigationController.transitionCoordinator?
        .viewController(forKey: .from),
      !navigationController.viewControllers
        .contains(dismissedViewController) else {
      return
    }
    performOnDismissed(for: dismissedViewController)
  }
}

This is just like the implementation from NavigationRouter in the playground: You check if the from view controller has been popped, and if so, call performOnDismissed to execute its on-dismiss closure.

Of course, you also need to set navigationController.delegate to the ModalNavigationRouter. Add the following to the end of the init(parentViewController:), right before the closing method brace:

navigationController.delegate = self

Using the PetAppointmentBuilderCoordinator

Fantastic! You’ve created all of the necessary pieces to display the schedule-a-visit flow. You now just need to put them all together.

Remember the TO-DO within the HomeCoordinator? Yep, this is where you’ll trigger the PetAppointmentBuilderCoordinator flow.

Open HomeCoordinator.swift and replace the TO-DO comment with the following:

let router =
  ModalNavigationRouter(parentViewController: viewController)
let coordinator =
  PetAppointmentBuilderCoordinator(router: router)
presentChild(coordinator, animated: true)

You here create a new ModalNavigationRouter, which you then in turn pass use to create a new PetAppointmentBuilderCoordinator and pass this to presentChild(_:animated:) to kick off this flow.

Build and run, and then tap Schedule Visit to see the schedule-a-visit flow in action!

Key points

You learned about the coordinator pattern in this chapter. Here are its key points:

  • The coordinator pattern organizes flow logic between view controllers. It involves a coordinator protocol, concrete coordinator, router protocol, concrete router and view controllers.
  • The coordinator defines methods and properties all concrete coordinators must implement.
  • The concrete coordinators know how to create concrete view controllers and their order.
  • The router defines methods all concrete routers must implement.
  • The concrete routers know how to present view controllers.
  • The concrete view controllers are typical view controllers, but they don’t know about other view controllers.
  • This pattern can be adopted for only part of an app or used across an entire application.

Where to go from here?

The coordinator pattern is a great pattern for organizing long-term or very complex apps. It was first introduced to the iOS community by Soroush Khanlou. You can learn more about this pattern’s roots in his blog post about it here:

There are also several other structural and architectural patterns similar to Coordinator. One example is VIPER, which further separates objects by responsibility. You can learn more about it in this writeup on objc.io:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.