A simple Coordinator Pattern for iOS App
If you tried and gave up because of the Coordinators’ complexity, check this implementation
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
instantiatesDetailsViewContoller
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 Coordinator
s — 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!