UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to use the coordinator pattern in iOS apps

Simplify your navigation and your view controllers

Paul Hudson       @twostraws

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

This is part 1 in a series of tutorials on fixing massive view controllers:

  1. How to use the coordinator pattern in iOS apps
  2. How to move data sources and delegates out of your view controllers
  3. How to move view code out of your view controllers

View controllers work best when they stand alone in your app, unaware of their position in your app’s flow or even that they are part of a flow in the first place. Not only does this help make your code easier to test and reason about, but it also allows you to re-use view controllers elsewhere in your app more easily.

 

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. This is a pattern I learned from Soroush Khanlou – folks who’ve heard me speak will know how highly I regard Soroush and his work, and coordinators are just one of many things I’ve learned reading his blog.

That being said, before I continue: I should emphasize this is the way I use coordinators in my own apps, so if I screw something up it’s my fault and not Soroush’s!

 

Prefer video? The screencast below contains everything in this article and more – subscribe to my YouTube channel for more like this.

 

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

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…

Coordinators in action

Start by creating a new iOS app in Xcode, using the Single View App template. I named mine “CoordinatorTest”, but you’re welcome to use whatever you like.

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 any child coordinators. We won’t need child coordinators here, but I’ll still add a property for them so you can expand this with your own code.
  2. 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.
  3. 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.

In Xcode, press Cmd+N to create a new Swift File called Coordinator.swift. Give it this content to match the requirements above:

import UIKit

protocol Coordinator {
    var childCoordinators: [Coordinator] { get set }
    var navigationController: UINavigationController { get set }

    func start()
}

While we’re making protocols, I usually add a simple Storyboarded protocol that lets me create view controllers from a storyboard. As much as I like using storyboards, I don’t like scattering storyboard code through my project – getting all that out into a separate protocol makes my code cleaner and gives you the flexibility to change your mind later.

I don’t recall where I first saw this approach, but it’s straightforward to do. We’re going to:

  1. Create a new protocol called Storyboarded.
  2. Give that protocol one method, instantiate(), which returns an instance of whatever class you call it on.
  3. Add a default implementation for instantiate() that finds the class name of the view controller you used it with, then uses that to find a storyboard identifier inside Main.storyboard.

This relies on two things to work.

First, when you use NSStringFromClass(self) to find the class name of whatever view controller you requested, you’ll get back YourAppName.YourViewController. We need to write a little code to split that string on the dot in the center, then use the second part (“YourViewController”) as the actual class name.

Second, whenever you add a view controller to your storyboard, make sure you set its storyboard identifier to whatever class name you gave it.

Create a second new Swift file called Storyboarded.swift, then give it the following protocol:

import UIKit

protocol Storyboarded {
    static func instantiate() -> Self
}

extension Storyboarded where Self: UIViewController {
    static func instantiate() -> Self {
        // this pulls out "MyApp.MyViewController"
        let fullName = NSStringFromClass(self)

        // this splits by the dot and uses everything after, giving "MyViewController"
        let className = fullName.components(separatedBy: ".")[1]

        // load our storyboard
        let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)

        // instantiate a view controller with that identifier, and force cast as the type that was requested
        return storyboard.instantiateViewController(withIdentifier: className) as! Self
    }
}

We already have a view controller provided by Xcode for this default project. So, open ViewController.swift and make it conform to Storyboarded:

class ViewController: UIViewController, Storyboarded {

Now that we have a way to create view controllers easily, we no longer want the storyboard to handle that for us. In iOS, storyboards are responsible not only for containing view controller designs, but also for configuring the basic app window.

We’re going to allow the storyboard to store our designs, but stop it from handling our app launch. So, please open Main.storyboard and select the view controller it contains:

  1. Use the attributes inspector to uncheck its Initial View Controller box.
  2. Now change to the identity inspector and give it the storyboard identifier “ViewController” – remember, this needs to match the class name in order for the Storyboarded protocol to work.

The final set up step is to stop the storyboard from configuring the basic app window:

  1. Choose your project at the top of the project navigator.
  2. Select “CoordinatorTest” underneath Targets.
  3. Look for the Main Interface combo box – it should say “Main”.
  4. Delete “Main”, leaving Main Interface blank.

That’s all our basic code complete. Your app won’t actually work now, but we’re going to fix that next…

Creating and launching our coordinator

At this point we’ve created a Coordinator protocol defining what each coordinator needs to be able to do, a Storyboarded protocol to make it easier to create view controllers from a storyboard, then stopped Main.storyboard from launching our app’s user interface.

The next step is to create our first coordinator, which will be responsible for taking control over the app as soon as it launches.

Create a new Swift File called MainCoordinator.swift, and give it this content:

import UIKit

class MainCoordinator: Coordinator {
    var childCoordinators = [Coordinator]()
    var navigationController: UINavigationController

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

    func start() {
        let vc = ViewController.instantiate()
        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 has an empty childCoordinators array to satisfy the requirement in the Coordinator protocol, but we won’t be using that here.
  3. It also has a navigationController property as required by Coordinator, along with an initializer to set that property.
  4. 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
}

If everything has gone to plan, you should be able to launch the app now and see something.

At this point you’ve spent about 20 minutes but don’t have a whole lot to show for your work. Stick with me a bit longer, though – that’s about to change!

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.

First, we need some dummy view controllers that we can display. So, press Cmd+N to create a new Cocoa Touch Class, name it “BuyViewController”, and make it subclass from UIViewController. Now make another UIViewController subclass, this time called “CreateAccountViewController”.

Second, go back to Main.storyboard and drag out two new view controllers. Give one the class and storyboard identifier “BuyViewController”, and the other “CreateAccountViewController”. I recommend you do something to customize each view controller just a little – perhaps add a “Buy” label to one and “Create Account” to the other, just so you know which one is which at runtime.

Third, we need to add two buttons to the first view controller so we can trigger presenting the others. So, add two buttons with the titles “Buy” and “Create Account”, then use the assistant editor to connect them up to IBActions methods called buyTapped() and createAccount().

Fourth, 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?

While you’re in BuyViewController and CreateAccountViewController, please also take this opportunity to make both of them conform to the Storyboarded so we can create them more easily.

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

func start() {
    let vc = ViewController.instantiate()
    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 buySubscription() {
    let vc = BuyViewController.instantiate()
    vc.coordinator = self
    navigationController.pushViewController(vc, animated: true)
}

func createAccount() {
    let vc = CreateAccountViewController.instantiate()
    vc.coordinator = self
    navigationController.pushViewController(vc, animated: true)
}

Those methods are almost identical to start(), except now we’re using BuyViewController and CreateAccountViewController 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 buyTapped() and createAccount() methods of the ViewController class.

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

@IBAction func buyTapped(_ sender: Any) {
    coordinator?.buySubscription()
}

@IBAction func createAccount(_ sender: Any) {
    coordinator?.createAccount()
}

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.

I have a second article that goes into more detail on common problems people face with coordinators – click here to read my advanced coordinators tutorial.

If you’re keen to learn more about design patterns in Swift, you might want to look at my book Swift Design Patterns.

And finally, I want to recommend once again that you visit Soroush Khanlou’s blog, khanlou.com, because he’s talked extensively about coordinators, controllers, MVVM, protocols, and so much more:

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.6/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.