How To Use The Coordinator Design Pattern in iOS – Swift 5.5

Update January 2022: This tutorial is updated to use Xcode 13.2.1 and Swift 5.5

If you are doing iOS for a while, you surely came across the Coordinator pattern. What I like the most about it is that I don’t have to change the traditional MVC architecture entirely but instead enhance it with the Coordinator pattern to make the view controller as light as possible and ensure more clarity to my code in addition to a nice separation of concerns.

In this quick tutorial you will learn how a view controller communicates with its relevant Coordinator and delegate to it the event handling task so that the view controller can focus on what matter the most to it: User Interface.

First download the starter project here and open it with Xcode.

Run the project to explore what it is all about, the structure is pretty simple, a view controller is embedded into a navigation controller, on button click, a new screen is pushed in the navigation controller.

The navigation controller is initialised in the SceneDelegate file.

Select HomeViewController.swift and you will notice that the routing logic to the next screen is implemented in the view controller layer.

@IBAction func onShowDetailsClick(_ sender: Any) {
    let viewController = DetailsViewController()
    self.navigationController?.pushViewController(viewController, animated: true)
}

That means the view controller knows exactly what to do when the button click is triggered and even how to do it (push to navigation controller in this case).

via GIPHY

This will become worse once the project gets bigger, for example if you will need to query the server for data and persist that data locally, such kind of tasks will be done in the view controller, right?

This way the view controller responsibilities will be charged with many tasks including UI setup, networking and data persistence.

This is the straight road to the famous MassiveViewController..

Time to change that!

The idea is that a view controller should never worry about routing or requesting a remote resource or dealing with data persistence.

Ideally, a view controller would only worry about displaying data and reporting user actions to another layer, this new layer is called Coordinator.

The Coordinator pattern is nothing but a tiny protocol with one required method.

Create a new Swift file (File menu -> New -> File… -> Swift File), name it Coordinator and declare the following protocol inside:

protocol Coordinator {
   var childCoordinators: [Coordinator] { get set }
   func start()
}

And that’s it! Now in order to use the Coordinator pattern, your class need to conform to the Coordinator protocol.

Note the start() function is to bootstrap and launch the coordinator lifecycle while the childCoordinators variable is used to store references of child coordinators in order to deallocate them easily from memory whenever a coordinator is no longe run in use.

You will create a Coordinator that manage the first screen and another one for the details screen. From the File menu create a new Swift File and name it HomeCoordinator.

Place the following code inside:

class HomeCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []

    func start() {
    }
}

Usually you use the start method to instantiate the view controller that is managed by this Coordinator. However, the home screen is currently instantiated without relying on the Coordinator factory.

So let’s change this to make the Coordinator load the Home screen instead.

From the Project navigator, select the SceneDelegate.swift file and change the implementation of the scene(_:willConnectTo:options:) delegate method to the following:

 //1
 let window = UIWindow(windowScene: windowScene)
 let homeCoordinator = HomeCoordinator(window: window)
 self.window = window
 self.homeCoordinator = homeCoordinator
 //2
 homeCoordinator.start()

//1 As you can see, you now take care manually of the home screen loading, you pass the window reference as a dependency to the home coordinator in order to use it later on to load the view, passing the window to the Coordinator this way is called Dependency Injection or DI shortly.

//2 Call the start() function will bootstrap the home coordinator factory and load the Home screen.

Always inside the SceneDelegate, add the following property declaration at the top of the file, right after the class opening bracket.

var homeCoordinator: HomeCoordinator?

Now switch to the HomeCoordinator.swift, make sure to import UIKit at the top of the file, and make the following changes:

//1
private let window: UIWindow
private let rootViewController: UINavigationController
//2
init(window: UIWindow) {
    self.window = window
    self.rootViewController = UINavigationController()
}
//3
func start() {
    window.rootViewController = rootViewController
    let homeViewController = HomeViewController()
    rootViewController.pushViewController(homeViewController, animated: true)
    window.makeKeyAndVisible()
}

//1 Here you declare the stored properties you need

//2 The initialiser is the best place to assign the dependencies of your object, you use it to initialise the navigation controller that will be used in the start method and will act as the presented of the view controller.

//3 Here you will assemble the puzzle, you basically embed the home view controller in the navigation stack, assign the latter as the root of the window and kick it on the screen :]

Build and run the app, the home screen should render correctly ;]

Click the button and you should be able to move to the next screen as you used to do earlier.

As discussed earlier, the HomeViewController is in charge of deciding how and where to move once you click on the button. You clearly notice this in the onShowDetailsClick function implementation.

You are going to change this, as a result the HomeCoordinator will be responsible of deciding this behaviour.

Coordinator – View Controller communication

There is two common ways to make the view controller report user interaction to its relevant Coordinator: Delegation pattern or closure callback.

In this tutorial you are going to use a closure callback.

Select the HomeViewController.swift and declare a callback as following:

var moveToDetailsHandler: (() -> Void)?

Now change the implementation of the onShowDetailsClick action method to the following:

@IBAction func onShowDetailsClick(_ sender: Any) {
    moveToDetailsHandler?()
}

Now move back to the HomeCoordinator.swift file and add the following code inside the start method (right after you instantiate the home view controller):

homeViewController.moveToDetailsHandler = {
}

At this stage, the Coordinator is ready to intercept the user action. You can add a print statement inside the closure body and run the app to verify it. Good work so far 💪🏻

By using the Coordinator pattern, you agree that the entry point for each screen/view controller should be through its Coordinator, in this case, you want to display the details screen.

You can easily instantiate the details view controller and use the root view controller to push it to the navigation stack and show it to the user.

However, we want to keep things clean and delegate all this to the details Coordinator instead.

Let’s add the Coordinator class that will handle this, select File-> New -> File from the menu and choose Swift File from the template panel, name the file DetailsCoordinator and save it to the project target.

Open the newly created file and change its content to the following:

import UIKit

public class DetailsCoordinator: Coordinator {
    var childCoordinators: [Coordinator] = []

    //1
    private let presentingViewController: UINavigationController

    init(presentingViewController: UINavigationController) {
        self.presentingViewController = presentingViewController
    }
    //2
    func start() {
        let detailsViewController = DetailsViewController()
        presentingViewController.pushViewController(detailsViewController, animated: true)
    }
}

//1 The stored property called presentingViewController will, later on, be injected from the HomeCoordinator, make sense, right?
//2 You instantiate the details view controller and push it to the presenting view controller navigation stack

Now you will finish up by calling the DetailsCoordinator start method from the HomeCoordinator to kick off the details screen ;]

Move back to the HomeCoordinator.swift file and change the moveToDetailsHandler closure body to the following:

homeViewController.moveToDetailsHandler = { [weak self] in
    guard let rootViewController = self?.rootViewController else {
        fatalError("rootViewController is nil")
    }
    let coordinator = DetailsCoordinator(presentingViewController: rootViewController)
    self?.childCoordinators.append(coordinator)
    coordinator.start()
}

Nothing fancy here, you basically instantiate the DetailsCoordinator and add it as child coordinator of the home coordinator, the rootViewController being the only dependency you sent to its initialiser, then you simply call the factory method start which will instantiate the details view controller and show it on the screen.

That’s it! Run the project and enjoy your work ;]

Deallocating unneeded coordinators

When the details screen is dismissed (ie: returning to home screen) the details view controller is deallocated automatically by the system. But what about the details coordinator?

Well, the details coordinator needs to be manually deallocated otherwise it will remain in memory for an undefined amount of time. A good memory management requires you to handle this step manually.

First select DetailsViewController.swift and make the following changes:

var dismissDetailsHandler: (() -> Void)?

override func viewWillDisappear(_ animated: Bool) {
    dismissDetailsHandler?()
}

As you did with the closure handling the button action to move to the details screen, you use the same pattern to alert the details coordinator that the view is about to be dismissed.

Select DetailsCoordinator.swift file and make the following changes:

var dismissCoordinatorHandler: (() -> Void)?

// Inside the start() function
detailsViewController.dismissDetailsHandler = {[weak self]      
   self?.dismissCoordinatorHandler?()
}

At this point the coordinator is aware of the dismiss action reported at the details view controller, you just added an additional handler and called it to inform the home coordinator this time ;]

Finally, select the HomeCoordinator.swift and perform the following changes to its start() function.

coordinator.dismissCoordinatorHandler = { [weak self] in
    guard let self = self else { return }
    self.childCoordinators = self.childCoordinators.filter({ $0 !== coordinator })
}

You can debug the code to double check that the childCoordinators array is no longer storing the details coordinator reference after dismissing the details screen. To do this change the above code to the following to add some print statements:

coordinator.dismissCoordinatorHandler = { [weak self] in
    guard let self = self else { return }
    print(self.childCoordinators)
    self.childCoordinators = self.childCoordinators.filter({ $0 !== coordinator })
    print(self.childCoordinators)
}

That’s it, build and run, you can check the childCoordinators array content in the console.

Before releasing the details coordinator:

[CoordinatorPatternDemo.DetailsCoordinator]

After releasing the details coordinator:

[]

Where to go from here?

You can download the final project here.

In this tutorial, you just learned :
– How to use Coordinator
– How a Coordinator can make your code respect the Single responsibility principle by delegating navigation work from the view controller to its Coordinator and keep the view controller focus on user interaction interception.
– How to use Dependency Injection and how this make you code clear and transparent.

For more details, I recommend you watch the excellent presentation of the Coordinator pattern here.

What Architectural pattern you are confortable to use in your project? Let me know in the comments section below :]

Malek
iOS developer with over than 11 years of extensive experience working on several projects with different sized startups and corporates.