Coordinator Pattern is one of the ways to implement a navigation control flow on iOS applications. Coordinators make it easy to test and encapsulate the navigation logic; it also helps to create reusable components, to use them on horizontal(push) or vertical(present) flows. It is a great place to implement deep linking and to provide dependencies between flows.
However, you have to be careful to handle lifecycles of the coordinators in order to avoid memory leaks. Especially back button taps, swipe back and iOS 13 dismiss gesture events can create memory leaks if those events are not handled properly.
To handle lifecycles of the coordinators; you could bind it onto view controller by holding them inside view controllers, or carefully detect pop and dismiss events to release them from their parent coordinators.
Using reactive streams to bind coordinators together makes it easy to handle those events and react to them.
Let’s start with the BaseCoordinator definition;
Each Coordinator is bound to its source view controller which can be defined directly or wrapped inside a navigation controller on the source property. BaseCoordinator uses the router to perform navigation actions and holds child coordinators to release them from memory after receiving a single event which could be a response from the child coordinator or a system call that can be triggered by pop or dismiss events.
It has basic push, present, setViewController and setRoot methods to determine the type of the navigation action, to perform on the passed coordinator.
Router class implements system functions of the navigation controller such as push, present, pop and dismiss operations and calls closures, so coordinator can release its child coordinators. I wanted to hold release closures of child view controllers on tied custom NavigationController so I can freely pass around related NavigationController and create the Router on the spot. If parent navigation controller is passed into the child coordinator, then all of its operation will be performed on it. If we want to present another view controller on top of the presented view controller, then we have to pass freshly created NavigationController into the child coordinator to present and use it inside our new flows.
Lets continue with the implementation of following flow;
Initially, AppCoordinator is called to start the SplashCoordinator using setRoot method. After 1 second SplashCoordinator sends an event back to the AppCoordinator, to continue the execution and to start the TabCoordinator using the same method. In the meantime SplashCoordinator is released inside the coordinate(to:) function due to receivedOutput event.
LizardCoordinators don’t publish any events back to the TabCoordinator by returning custom Never publisher, which is Empty(completeImmediately: false) publisher under the hood. However it observes the button event to present a ComposeCoordinator on the vertical flow. By passing freshly created NavigationController on the ComposeCoordinator, router uses that navigation controller as a source view controller to present it.
It is also used to create new Router inside the ComposeCoordinator which then will present another ComposeCoordinator recursively on command.
WakingUpCoordinator observes the openUrlSubject to present Waking Up application website on a SafariViewController via SafariCoordinator.
It can also push and observe output of the SettingsCoordinator.
BTW if you can’t afford it, you can request a free account access from Waking Up application via this link.
SettingsCoordinator is pushed on the horizontal flow inside the WakingUpCoordinator and it returns a logoutAction event through the WakingUpCoordinator into AppCoordinator, to perform logout and start application flow again with the SplashCoordinator.
We could just push our SettingsCoordinator on the horizontal flow indefinitely and pass our logout action back to the navigation stack.
We could easily use ComposeCoordinator to present it inside the SettingsCoordinator in the same way on our LizardCoordinator.
You can access above implementation from this repository
Extracting navigation logic outside of the view controllers helps to solve the Massive View Controller problem on iOS development. Coordinators are also great place to implement dependency injection besides the navigation logic. We could easily provide dependencies inside them and pass necessary parameters through the navigation flows.
I tried to implement the BaseCoordinator to handle most of the navigation commands. I wanted to easily push and present coordinators without getting involved in dismissing action and only focusing on the events that our business logic concerns.
I hope that this article could be a starting point for you to implement the coordinator pattern using Apple Combine framework. You could add a couple of changes and have fully functional and testable coordinators on your project.
Let me know if you have any feedbacks or comments.
Thanks for reading.
Using custom initializers inside view controllers on iOS 13 or later.
PrefixUntilOutput and Custom “TakeUntil” Publisher Operators in Apple Combine Framework
Combine framework was introduced at WWDC 2019 as a new way of reactive programming on Apple platforms. It has native…
Taming Great Complexity: MVVM, Coordinators and RxSwift
Last year our team started using Coordinators and MVVM in a production app. At first it looked scary, but since then…
Coordinator & MVVM - Clean Navigation and Back Button in Swift
After introducing how to implement Coordinator pattern with an MVVM structure, it feels natural for me to go further…