A simple Coordinator Pattern for iOS App

Photo by Visual Design on Unsplash

The coordinator pattern becomes quite popular in the iOS world. Many developers understand, that keeping navigation logic inside ViewControllers and/or ViewModels makes code harder to maintain, not mentioning about breaking the Single Responsibility Principle. And while working on Storyboards and Segues makes it easy to create a prototype, it does not scale well and becomes a nightmare as the app is growing.

The Coordinator pattern

The coordinator pattern is simple in its idea. The two main goals to be achieved are as follows:

  • Decouple navigation logic from a View layer — push, present etc. — by moving it to a separate layer
  • Decouple dependency between scenes — if some MainViewContoller instantiates DetailsViewContoller then, on code level, it depends on it.

Most Basic Coordinators that can be found on the internet begins with this:

protocol Coordinator {
func start()
}

It is a nice starting point, so let’s leave it as is. The idea behind it is simple — the only thing you can do with a Coordinator is starting it, which should lead to a new scene being presented. Fair enough, now let’s check for possible implementations.

Typical implementation

final class FeatureMainCoordinator: Coordinator {    init(navigationContorller: UINavigationContoller) {
self.navigationController = navigationController
}
func start() {
let viewController = /* instantiate feature scene */
navigationController.push(viewController, animated: true)
}
func showDetails() {
let detailsCoordinator = FeatureDetailsCoordinator(
navigationController: navigationController
)
detailsCoordinator.start()
}
}

That looks pretty easy — in this article, we will skip who and how is responsible for calling showDetails. However, there is one issue with that implementation. Assuming, that FeatureDetailsCoordinator implementation is similar to above, there is nobody holding detailsCoordinator — which means it will be deallocated from memory just after returning from showDetails.

To address the above, there are two common approaches.

Store Coordiantor inside ViewController

Easy way to fix deallocation could be following:

let detailsViewController = FeatureDetailsViewController(
coordinator: detailsCoordinator
)

So — it will work, but it makes two-way dependency in code. Since the Coordinator is responsible for creating a ViewController it is dependent on it. If we pass Coordinator inside ViewController, then we’re introducing another way dependency. Not ideal.

Keep stack of Coordinators

In another solution MyFeatureCoordinator can store a reference to DetailsCoordinator:

final class FeatureMainCoordinator: Coordinator {    var detailsCoordinator: FeatureDetailsCoordinator?    func showDetails() {
detailsCoordinator = FeatureDetailsCoordinator(
navigationController: navigationController
)
detailsCoordinator?.start()
}
}

It’s not complicated, at least until we’re talking about going forward. Things get complicated when a user taps on a back button as we should release detailsCoordinator from memory. How to do that? Well… I won’t tell you, if you are reading this article you may already know some popular ideas, like listening to UINavigationContollerDelegate or overriding back button. If you’re interested in more — try to google Swift Coordinator handle back.

Also, in the above example we do have a single screen we can navigate to from FeatureMainCoordinator. What if some Coordinator has more than one screen it can navigate to? You can create var childCoordinators: [Coordinator] and then…

Ok, wait, didn’t we were about to simplify navigation logic? We’re on the wrong path then.

The need of maintaining two navigation stacks — one in UINavigationController, second in Coordinators — is one of the biggest cons against the Coordinator pattern. So does Coordinator needs to be complicated? Does the price of having decoupled navigation logic have to be so high?

Using UIKit navigation stack for Coordinators

We agreed that we want to avoid maintaining two navigation stacks. We cannot resign from the stack provided by UINavigationController, at least not if we want to keep the solution simple. So, only one thing left — let’s push a Coordinator!

final class MyFeatureCoordinator: UIViewController, Coordinator {    init(navigationContorller: UINavigationContoller) {
self.navigationController = navigationController
}
func start() {
let contentViewController = /* instantiate scene */
embed(contentViewController)
navigationController.push(self, animated: true)
}
private func embed(contentViewContoller: UIViewController) {
contentViewController.willMove(toParent: self)
addChild(contentViewController)
contentViewController.didMove(toParent: self)
view.addSubview(contentViewController.view)
/* few lines of autolayout here */
}
func showDetails() {
let detailsCoordinator = MyFeatureDetailsCoordinator(
navigationController: navigationController
)
detailsCoordinator.start()
}
}

Ok, I hear you: Coordinator being a UIViewContoller 😟? But why? I know it might be weird at first sight but.. in the end, there is no magic here, right? Also, our goals are fulfilled — logic about navigation is decoupled and moved to dedicated class, the scene does not know about the existence of Coordinator. Sounds like a win to me 😉

The main advantage here is that instead of a completely separate stack to maintain, we are using simple composition — the Coordinator is keeping a ViewController as its child and then reusing the existing stack.

Bonus advantage

In the proposed solution there is one more advantage. If you already working on an app that suffers due to navigation logic kept inside ViewControllers (not dedicated ones 😉) then there is nothing holding you back from starting using Coordinators defined as above from Today. Since Coordinator is also a UIViewController, you can push your newly created scene from the existing UIViewController. At least that new scene will be clean 💪

Final thoughts

I like the above mostly because of its simplicity. Hope, that you’ll like it too. Even if the proposed solution is not intuitive at first glance, it should not be a problem for any developer to understand, use and maintain. And that, after all, is what the KISS is all about!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Tomasz Załoga

Tomasz Załoga

I'm an iOS developer with 7 years of experience, recently happily learning SwiftUI and Vapor. Enjoyed being part of the team developing mobile apps for Toyota.