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:
-
The coordinator is a protocol that defines the methods and properties all concrete coordinators must implement. Specifically, it defines relationship properties,
childrenandrouter. It also defines presentation methods,presentanddismiss.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.
-
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.
-
The router is a protocol that defines methods all concrete routers must implement. Specifically, it defines
presentanddismissmethods for showing and dismissing view controllers. -
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.
-
The concrete view controllers are typical
UIViewControllersubclasses 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:
-
You first define two
presentmethods. The only difference is one takes anonDismissedclosure, and the other doesn’t. If provided, concrete routers will execute theonDismissedwhenever a view controller is dismissed, for example via a “pop“ action in the case of a concrete router that uses aUINavigationController. -
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, callingdismisson aparentViewControlleror whatever action is necessary per the concrete router’s implementation. -
You lastly define a default implementation for
present(_:animated:). This simply calls the otherpresentby passingnilforonDismissed.
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:
-
You declare
NavigationRouteras a subclass ofNSObject. This is required because you’ll later make this conform toUINavigationControllerDelegate. -
You then create these instance properties:
-
navigationControllerwill be used to push and pop view controllers. -
routerRootControllerwill be set to the last view controller on thenavigationController. You’ll use this later to dismiss the router by popping to this. -
onDismissForViewControlleris a mapping fromUIViewControllerto on-dismiss closures. You’ll use this later to perform an on-dismiss actions whenever view controllers are popped.
- You lastly create an initializer that takes a
navigationController, and you set thenavigationControllerandrouterRootControllerfrom 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:
-
Within
present(_:animated:onDismissed:), you set theonDismissedclosure for the givenviewControllerand then push the view controller onto thenavigationControllerto show it. -
Within
dismiss(animated:), you verify thatrouterRootControlleris set. If not, you simply callpopToRootViewController(animated:) on thenavigationController. Otherwise, you callperformOnDismissed(for:)to perform the on-dismiss action and then pass therouterRootControllerintopopToViewController(_:animated:)on thenavigationController. -
Within
performOnDismiss(for:), you guard that there’s anonDismissfor the givenviewController. If not, you simplyreturnearly. Otherwise, you callonDismissand remove it fromonDismissForViewController.
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:
-
You declare relationship properties for
childrenandrouter. You’ll use these properties to provide default implementations within anextensiononCoordinatornext. -
You also declare required methods for
present,dismissandpresentChild.
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:
-
To
dismissa coordinator, you simply calldismisson itsrouter. This works because whoever presented the coordinator is responsible for passing anonDismissclosure to do any required teardown, which will be called by the router automatically.Remember how you wrote all that logic within
NavigationRouterfor handling popping and dismissing? This is why you did that! -
Within
presentChild, you simply append the givenchildtochildren, and then callchild.present. You also take care of removing thechildby callingremoveChild(_:)within the child’sonDismissedaction, and lastly, you call the providedonDismissedpassed 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:
-
First, you declare properties for
childrenandrouter, which are required to conform toCoordinatorandRouterrespectively. -
Next, you create an array called
stepViewControllers, which you set by instantiating severalStepViewControllerobjects. This is a simple view controller that displays a button with a multiline label.You set the view controllers’
textsto 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! -
Next, you declare a property for
startOverViewController. This will be the last view controller displayed and will simply show a button to “start over.” -
Next, you create a designated initializer that accepts and sets the
router. -
Finally, you implement
present(animated:, onDismissed:), which is required byCoordinatorto 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:
-
First, you create
homeViewController, and then use this to createnavigationController. 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. -
Next, you create the
routerusing thenavigationController, and in turn, create thecoordinatorusing therouter. -
If you open HomeViewController.swift, you’ll see it has a single button that ultimately calls its
onButtonPressedclosure. Here, you sethomeViewController.onButtonPressedto tell thecoordinatortopresent, which will start its flow. -
Finally, you set the
PlaygroundPage.current.liveViewto thenavigationController, which tells Xcode to display thenavigationControllerwithin 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:
-
You first create
lazyproperties forcoordinator,router, andwindow. -
Then within
application(_:didFinishLaunchingWithOptions:), you callcoordinator.presentto start theHomeCoordinatorflow.
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:
-
Within
selectVisitTypeViewController(_:didSelect:), you first setbuilder.visitType. -
You then switch on the selected
visitType. -
If the
visitTypeiswell, you callpresentNoAppointmentViewController()to show aNoAppointmentRequiredViewController. -
If it is
sick, you callpresentSelectPainLevelCoordinator()to show aSelectPainLevelViewController.
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:
-
Within
selectPainLevelViewController(_:didSelect:), you first set thebuilder.painLevel. -
You then switch on the selected
painLevel. -
If the case matches
noneorlittle, you callpresentFakingItViewController()to show aFakingItViewController. -
If the case matches
moderate,severeorworstPossible, you canpresentNoAppointmentViewController()to show aNoAppointmentRequiredViewController.
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:
-
Within
fakingItViewControllerPressedIsFake(_:), you simply callrouter.dismiss(animated:)to exit out of the coordinator flow. -
Within
fakingItViewControllerPressedNotFake(_:), you again callpresentNoAppointmentViewController()to show aNoAppointmentRequiredViewController.
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:
-
First, you declare
ModalNavigationRouteras a subclass ofNSObject. Being a subclass ofNSObjectis required because you’ll later make this conform toUINavigationControllerDelegate. -
Next, you create instance properties for
parentViewController,navigationControllerandonDismissForViewController. -
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:
-
In
present(_:animated:onDismissed:), you first setonDismissForViewControllerfor the given view controller to the given closure. You then check if thenavigationControllerdoesn’t have any view controllers. This means that you need to modally present the navigation controller, which you do by callingpresentModally(_:animated:). Otherwise, you callpushViewControlleron thenavigationController. -
Within
presentModally(_:animated:), you first pass the view controller toaddCancelButton(to:)in order to set up a Cancel button on the view controller. If the button is tapped, it will callcancelPressed(), perform the on-dismiss action and ultimately calldismiss(animated:). -
Next, still within
presentModally(_:animated:), you set theviewControllerson thenavigationControllerusing the passed-inviewController. You then callpresenton theparentViewControllerin order to modally present thenavigationController. -
Within
dismiss(animated:), you callperformOnDismissedpassingnavigationController.viewControllers.first. You then tell theparentViewControllertodismissits presented view controller, which is thenavigationController. -
performOnDismissed(for:)is exactly the same as the method inNavigationRouter: It checks if there’s an existingonDismissclosure for the view controller, executes it if found, and finally removes the closure fromonDismissForViewController.
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: