In 2015, at the well-known iOS conference NSSpain’15, Soroush Khanlou made an introductory talk “Presenting Coordinators”. During this talk he described the whole idea of Coordinators and showed some examples written in Objective-C. The general idea is taken from the book “Patterns of Enterprise Application Architecture” which explains the Application Controller pattern and was adopted by Soroush for mobile development.
You can check out the video of the talk here.
I enjoyed this video a year ago and was really impressed by the whole idea. During a cold evening in February, I wrote an example project with my first iteration of this architecture approach. I used Swift as my main language, of course. After that, step by step I improved this approach and covered more and more use cases. After two months, I started integrating Coordinators to the production app. In the beginning, only a few features were driven by coordinators, but in the end, the skeleton of the app became fully driven by them. Later I added deep link support, push notifications, force touch features, and flexible flows as well, all of which were handled by coordinators. It is this experience that I want to share with you now.
If you want to skip my explanation and check out the code, here is the link. Let’s start with a bunch of questions.
What is flow in mobile applications?
Flow is a queue of screens that are logically chained. All of our screens can be divided by flows: auth flow, phone verify flow, booking flow, profile editing flow, etc.
How do we usually manage screens in flow?
Imagine that we have an auth flow and during the flow we need to verify a user’s phone.
Let’s take a look at such a scheme as an example:
How do we usually handle navigation in this flow?
In the most common case, the controller tells the router to push a new controller. This is how it looks in code:
The controller is completely chained with all neighbouring controllers and is trying to manage them. It looks like Gromozeka from the USSR’s cartoon:
The next common case is sending data between controllers.
Imagine we have a bunch of controllers and we need to share data between them. How do we accomplish this?
Great! We created a coupled flow. Now the project manager comes to us and says:
Another common reason to make flows independent is their ability to be reused in different places. For example, imagine you have a phone verify flow and you successfully use it as part of auth flow. Now, if you need to use the same flow in profile, you can.
Summary of the introduction
Approaches that directly handle the flow usually turn out terribly:
- controllers are hard to reuse
- every controller knows about other controllers
- hard to change flow
- hard to test
First of all, let’s introduce module. Module is one architectural element of MVC, MVP, MVVM, and Viper architecture approaches. In the simplest case, for MVC, module is a controller and, for Viper, an Interactor-Presenter-View.
What we need to do is orchestrate modules.
What is Coordinator?
Coordinator is an object that handles navigation flow and shares flow’s handling for the next coordinator after switching on the next chain. It means that coordinators should only keep navigation logic between screens. Coordinators can keep references to a storage, which holds data that is used by factories for creating modules, but they can never handle a module’s business logic and never drive a cell’s behavior. Cell interaction only happens when we push the next screen after tapping a cell.
Why should we use it?
The main goal is to have unchained module-to-module responsibility and to build modules completely independently from each other. After this iteration we can easily reuse them in different flows.
Returning to our previous example, let’s look at how our auth flow can be improved. First of all, we need to separate the two different flows: auth and phone verify. After splitting we can reuse them in any place we want.
Now we have two separated flows.
Let’s look under the hood.
What parts does the Coordinator consist of?
Let’s start with the general interface.
All Coordinators must conform to a protocol:
It means if you want to run a new flow you need to create a Coordinator using a factory and call start().
All of the Coordinators have individual finish blocks to support business rules:
For example, if a flow’s result should be creating Item, the Coordinator has the possibility to return it to its finish block. In this case, the Coordinator’s interface will be a composition of two protocols, Coordinator & CoordinatorOutput.
During auth flow, the user must verify the user’s phone. These are two separate flows (auth and phone verify), but when we run auth Coordinator we don’t care about this logic. We just create Coordinator, configure finish block, and call start().
What do we need for Coordinators to work?
We can easily change the number of classes we want to use, but the usual setup looks like this:
- Router, for navigation (router just routes! And it’s passive in our cases).
- Modules’ factory, for creating modules and injecting all dependencies.
- Coordinators’ factory (optional), in case we need to switch to a different flow.
- Storage (optional), only if we store any data and inject it into the modules.
The Coordinator’s body consists of two kinds of funcs. The first func, if you need to launch a new module, should be named with the “show” prefix:
What do we do in this first func? We ask factory to create a module, configure callbacks, and ask the router to push it.
The second func runs the new flow, since we need to switch between them. Its name always starts with the “run” prefix:
Factory creates the first flow’s module and coordinator. We push the module and the Coordinator runs the flow. Since the Coordinator has a finish block, we must configure deleting all dependencies, dismissing the module, and showing the created item as a result of the flow’s process.
All Coordinators inherit from a base Coordinator. The base Coordinator class keeps all dependency logic. We need it for memory management reasons, to keep strong references to all child coordinators, and to allow Coordinators to call functions on their children (this is how deep linking works).
The base coordinator class looks like this:
From this example, it’s understood that the parent coordinator has all of the children’s references.
Let’s talk about the router. Why should we use this pattern? Why can’t we keep UINavigationController’s reference and push by coordinator itself?
First of all, it breaks the Single Responsibility principle: coordinator manages flow but it is never responsible for routing.
Second, we could either support iPad or add custom screen transitions. Coordinator will never know about it, as it can continue to use the router’s protocol interfaces, like “push”, and not care about new transition animation. The implementation details of push is up to the router. As a result, we can create a composition of router classes for different devices, like iPhone, iPad, TV, or Watch, and have them conform to one general protocol.
The router’s funcs get Presentable protocol as a parameter:
Presentable looks like this:
It is made for flexibility and as well as abstraction. We can create a module as complicated as we want, but it is necessary to extract UIViewController for the router’s usage. It does not matter which approach the module will use for getting this controller.
I believe everybody knows the cases for which we should use this pattern. In our case, it works like Viper’s assembly, meaning factory is responsible for module’s building. It creates all objects, sets all dependencies, injects properties, and returns protocol.
In an easy case it would be just one controller:
We return protocol because it’s easy to test and, in this case, we follow the Open-Close principle.
We use two factory classes in our project for modules and coordinators. In our team, we endured a lot of holy wars deciding in which cases we should create a factory. Create a factory class for every flow, or keep all in one? The problem is, if we reuse a module in different flows, we must copy-paste these methods in different factories. So we decided to keep all modules in one factory class and all coordinators in another. For every flow, we create factory’s protocol with all necessary modules, like so:
Factory’s class should conform to all of these protocols:
For coordinators’ factory we create only one protocol, but every coordinator should have the possibility to create any another coordinator.
Storage keeps all the data that factory uses to create the next module.
For example, we want to create an object and our flow consists of three modules: A, B, and C. After some work, we retrieve a dictionary from Module A and a string from Module B. Finally, we send this data to Module C. Module C can then add some other data and send it to the server.
We can add some additional logic to storage for saving data on disc or format data as json.
This is probably enough for the first part. What benefits have we learned?
- controllers know nothing about other controllers
- controllers can be easily integrated in different flows
- controllers don’t send data to others
- easy to reuse
- simplified testing
I highly recommend you to download the project example.
Second part is here.