Coordinator Pattern in swift

Mahmoud Bassuni
7 min readApr 6, 2020

--

Using the coordinator pattern in iOS apps lets us remove the job of app navigation from our view controllers, helping make them more manageable , more reusable and testable via unit test , while also letting us adjust our app’s flow whenever we need .

In this article I want to provide you with a hands-on example of the coordinator pattern, which takes responsibility for navigation out of your view controllers and into a separate class.

Why do we need to change?

Let’s start by looking at code most iOS developers have written a hundred or more times:

if let vc = storyboard?.instantiateViewController(withIdentifier: "SomeVC") {
navigationController?.pushViewController(vc, animated: true)
}

In that kind of code, one view controller must know about, create, configure, and present another. This creates tight coupling in our application: you have hard-coded the link from one view controller to another, so and might even have to duplicate your configuration code if you want the same view controller shown from various places.

What happens if you want different behavior for iPad users, VoiceOver users, or users who are part of an A/B test? Well, your only option is to write more configuration code in your view controllers, so the problem only gets worse.

Even worse, all this involves a child telling its navigation controller what to do — our first view controller is reaching up to its parent and telling it present a second view controller.

To solve this problem cleanly, the coordinator pattern lets us decouple our view controllers so that each one has no idea what view controller came before or comes next — or even that a chain of view controllers exists.

Instead, your app flow is controlled using a coordinator, and your view communicates only with a coordinator. If you want to have users authenticate themselves, ask the coordinator to show an authentication dialog — it can take care of figuring out what that means and presenting it appropriately.

The result is that you’ll find you can use your view controllers in any order, using them and reusing them as needed — there’s no longer a hard-coded path through your app. It doesn’t matter if five different parts of your app might trigger user authentication, because they can all call the same method on their coordinator.

For larger apps, you can even create child coordinators or subcoordinators that let you carve off part of the navigation of your app. For example, you might control your account creation flow using one subcoordinator, and control your product subscription flow using another.

If you want even more flexibility, it’s a good idea for the communication between view controllers and coordinators to happen through a protocol rather than a concrete type. This allows you to replace the whole coordinator out at any point later on, and get a different program flow you could provide one coordinator for iPhone, one for iPad, and one for Apple TV, for example.

So, if you’re struggling with massive view controllers I think you’ll find that simplifying your navigation can really help. But enough of talking in the abstract — let’s try out coordinators with a real project

let me show you very very small example

First create 3 ViewControllers with their xibs with following

  • MainViewController
  • FirstViewController
  • SecondViewController

There are three steps I want to cover in order to give you a good foundation with coordinators:

  1. Designing two protocols: one that will be used by all our coordinators, and one to make our view controllers easier to create.
  2. Creating a main coordinator that will control our app flow, then starting it when our app launches.
  3. Presenting other view controllers.

Like I said above, it’s a good idea to use protocols for communicating between view controllers and coordinators, but in this simple example we’ll just use concrete types.

First we need a Coordinator protocol that all our coordinators will conform to. Although there are lots of things you could do with this, I would suggest the bare minimum you need is:

  1. A property to store the navigation controller that’s being used to present view controllers. Even if you don’t show the navigation bar at the top, using a navigation controller is the easiest way to present view controllers.
  2. A start() method to make the coordinator take control. This allows us to create a coordinator fully and activate it only when we’re ready.
import UIKit

protocol Coordinator {
var navigationController: UINavigationController { get set }

func start()
}

Creating and launching our coordinator

import UIKit

class MainCoordinator: Coordinator {
var navigationController: UINavigationController

init(navigationController: UINavigationController) {
self.navigationController = navigationController
}

func start() {
let vc = MainViewController.init()
navigationController.pushViewController(vc, animated: false)
}
}

Let me break down what all that code does…

  1. It’s a class rather than a struct because this coordinator will be shared across many view controllers.
  2. It also has a navigationController property as required by Coordinator, along with an initializer to set that property.
  3. The start() method is the main part: it uses our instantiate() method to create an instance of our ViewController class, then pushes it onto the navigation controller.

Notice how that MainCoordinator isn’t a view controller? That means we don’t need to fight with any of UIViewController’s quirks here, and there are no methods like viewDidLoad() or viewWillAppear() that are called automatically by UIKit.

Now that we have a coordinator for our app, we need to use that when our app starts. Normally app launch would be handled by our storyboard, but now that we’ve disabled that we must write some code inside AppDelegate.swift to do that work by hand.

So, open AppDelegate.swift and give it this property:

var coordinator: MainCoordinator?

That will store the main coordinator for our app, so it doesn’t get released straight away.

Next we’re going to modify didFinishLaunchingWithOptions so that it configures and starts our main coordinator, and also sets up a basic window for our app. Again, that basic window is normally done by the storyboard, but it’s our responsibility now.

Replace the existing didFinishLaunchingWithOptions method with this:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// create the main navigation controller to be used for our app
let navController = UINavigationController()
// send that into our coordinator so that it can display view controllers
coordinator = MainCoordinator(navigationController: navController)
// tell the coordinator to take over control
coordinator?.start()
// create a basic UIWindow and activate it
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = navController
window?.makeKeyAndVisible()
return true
}

Handling app flow

Coordinators exist to control program flow around your app, and we’re now in the position to show exactly how that’s done.

we need to add two buttons to the MainViewController so we can trigger presenting the others. So, add two buttons with the titles “vc1” and “vc2 ”, then use the assistant editor to connect them up to IBActions methods called openVC1() and openVC2().

then, all our view controllers need a way to talk to their coordinator. As I said earlier, for larger apps you’ll want to use protocols here, but this is a fairly small app so we can refer to our MainCoordinator class directly.

So, add this property to all three of your view controllers:

weak var coordinator: MainCoordinator?

Finally, open MainCoordinator.swift and modify its start() method to this:

func start() {
let vc = ViewController.init()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}

That sets the coordinator property of our initial view controller, so it’s able to send messages when its buttons are tapped.

At this point we have several view controllers all being managed by a single coordinator, but we still don’t have a way to move between view controllers.

To make that happen, I’d like you to add two new methods to MainCoordinator:

func openFirstViewController() {
let vc = FirstViewController.init()
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}
func openSecondViewController() {
let vc = SecondViewController.init()
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}

Those methods are almost identical to start(), except now we’re using FirstViewController and SecondViewController instead of the original ViewController. If you needed to configure those view controllers somehow, this is where it would be done.

The last step — the one that brings it all together — is to put some code inside the openVC1() and openVC2() methods of the ViewController class.

All the actual work of those methods already exists inside our coordinator, so the IBActions become trivial:

@IBAction func openVC1(_ sender: Any) {
coordinator?.openFirstViewController()
}
@IBAction func openVC2(_ sender: Any) {
coordinator?.openSecondViewController()
}

You should now be able to run your app and navigate between view controllers — all powered by the coordinator.

What now?

I hope this has given you a useful introduction to the power of coordinators:

  • No view controllers know what comes next in the chain or how to configure it.
  • Any view controller can trigger your purchase flow without knowing how it’s done or repeating code.
  • You can add centralized code to handle iPads and other layout variations, or to do A/B testing.
  • But most importantly, you get true view controller isolation: each view controller is responsible only for itself.

Thank you for reading! If you liked this article, please Share it so other people can read it too :)

You can catch me at:

Linkedin: mahmoud Bassuni

--

--