Coordinator Pattern’s Issues & What is RouteComposer

Eugene Kazaev
ITNEXT
Published in
14 min readApr 9, 2019

--

I am continuing the series of articles about the RouteComposer library that we use. In this article, I want to talk about the Coordinator Pattern. I was prompted to write this article by a discussion in one of the previous articles about the Coordinator Pattern.

The Coordinator Pattern, having been introduced recently, is gaining more and more popularity in the iOS developer community, and, in general, it is clear why. Out-of-the-box tools that UIKit provides are a rather unstandardised mess.

I previously raised issues in handling the fragmentation in the ways of building the view controller into the stack. To avoid repetition, If you want to read more, you can do so here.

Let’s be honest, at one point, even Apple realised that when they put view controllers at the centre of application development, they did not suggest any sensible way of integrating them into each other or transferring data between them. Having given solutions to this problem to the very same developers that implemented code completion in Xcode (joking), at some point Apple presented us with the storyboards and segues.

Later, Apple realised that they’re the only one that develop applications consisting of 2 or less screens. In the next iteration, they offered the opportunity to split storyboards into several parts as Xcode began crashing when the storyboard reached a certain size. Segues changed along with this concept in several iterations that aren’t necessarily compatible with each other. Their support is tightly woven into the massive UIViewController class, and, ultimately, we have ended up with this:

The number of force typecasts in this block of code is amazing. Along with the pure string constants in the storyboards themselves, for which Xcode does not offer any meaningful way to track them. The slightest desire to change anything in the process of navigation using the storyboard will allow you to compile the project without any effort, though it will crash into runtime with a cheerful crackling sound without any warning from Xcode. What you see is not what you get.

One can argue about the merits of these grey arrows in the storyboards, supposedly showing someone the connections between the screens. However, in my experience, and I deliberately interviewed several iOS developers from different companies, as soon as their projects grew beyond 5–6 screens, developers tried to find a more reliable solution, and finally began to keep the structure of the stack of controllers in their heads. This means if you need to add support for the iPad, another navigation model, or support for push-notifications, then everything becomes generally sad.

Since then, several attempts have been made to solve this problem — some of which turned into separate frameworks and some into separate architectural patterns.

Let’s go back to the Coordinator Pattern. For obvious reasons, you will not find any description in Wikipedia. It is not a standard programming / design pattern. Rather, it is a kind of abstraction that suggests you hide all this “ugly” code of creating and inserting a new controller view into the stack, storing references to the container of controllers and pushing data between controllers under the hood. The most useful article I found is raywenderlich.com. The Coordinator Pattern started to become popular after the 2015 NSSpain conference, when it was presented to the general public. You can find more details here and here.

I will briefly describe what it is before moving on.

In all interpretations the Coordinator Pattern approximately fits into this picture:

That is, the coordinator is a protocol:

protocol Coordinator {

func start()

}

All the “ugly” code is supposed to be hidden in the start function. In addition, the coordinator may have links to the child coordinators. That is, they have some possibility of composition, and, for example, you can replace one implementation with another. Sounds pretty elegant, right?

However, it becomes fairly inelegant quickly:

  1. Some implementations propose you transform the Coordinator from some creational pattern into something smarter that follows the stack of view controllers and make itself a delegate of the container view controller. For an example the coordinator of UINavigationController, to process tapping the ‘back’ button or swiping back and deleting the child coordinator. Naturally, only one object can be a delegate, which limits the ability to control the container view controller itself. Which leaves us at the conclusion to either build that logic in with the coordinator or to create the delegates of the coordinators to pass that logic to somebody else.
  2. Often the logic of creating the next controller depends on business logic. For example, to go to the next screen, the user must be logged into the system. Clearly this is an asynchronous process which involves presenting some intermediate screen with a login form and the login process itself can succeed or not. To avoid turning the Coordinator into a Massive Coordinator (by analogy with the Massive View Controller), we need a decomposition. That is, it is necessary to create a coordinator of the coordinator.
  3. Another problem faced by coordinators is that they are essentially just fasades for container view controllers such as UINavigationController, UITabBarController and so on. Someone must provide them the references to these controllers. If everything is more or less clear with the child coordinators, the initial coordinators of the chain are not so simple. Plus, when you change the navigation pattern, such as for A / B testing, refactoring and adaptation of such coordinators results in a separate headache. Especially if the type of container view controller changes.
  4. All of this becomes even more complicated when the application starts to support external events, which build view controllers. This includes push-notifications or universal links (the user clicks on a link in an email and continues to the appropriate screen of the application). There are other uncertainties that the Coordinator Pattern does not have an exact answer for. You need to know exactly which screen the user is on in order to show them the next screen requested by an external event.
    The simplest example is a chat application consisting of 3 screens:
    1. A chat list.
    2. the chat itself which is pushed into the navigation of the chat list’s navigation controller.
    3. The settings screen that can be presented modally.

    A user can be on one of these screens when they receive a push notification and tap on it. Here begins the uncertainty; if they are in the chat list, you need to push a chat screen with this particular sender; if they are already in the chat screen, then you need to switch it; and if they are already in the chat with the requested sender, then do nothing and update it. However, if the user is on the settings screen, it’s probably necessary to close it and do the previous steps. But what if you don’t close it and just show the chat modally above the settings? And what if the settings are in another tab, and not a modal? All these if / elses start to either smear the coordinators or go to another Mega-Coordinator in the form of a piece of spaghetti. Plus there are either active iterations of the stack of view controllers that will be involved in an attempt to determine where the user is at the moment, or an attempt to build an application that monitors its state, which, in itself, is not a very simple task simply based on the nature of the stack of controllers.
  5. The cherry on the cake is UIKit glitches. A banal example: UITabBarController which has UINavigationController with some other UIViewController in the second tab. The user causes some event in the first tab that requires you to switch the tab and push another view controller into the second tab’s UINavigationController. You need to do it in that specific order because if the user has never opened the second tab and the UINavigationController’s viewDidLoad has not been called before, the push method will fail leaving only a vague message in the console. So the coordinators cannot simply be made listeners of the events in the said example; they must work in a certain sequence. Meaning they must have knowledge of each other. This already contradicts the Coordinator’s first statement; that the coordinators do not know anything about the parent coordinator and are associated only with the child ones. This also limits their interchangeability.

This list can be continued but in general, it is clear that the Coordinator Pattern is a rather limited, poorly scalable, solution. If you look at it without rose-colored glasses, then it is just another way of decomposing a part of logic, that is usually written from the massive UIViewController, into another class. All attempts to make the Coordinator Pattern something more than a kind of Factory pattern and introduce another logic do not end well.

It is worth mentioning that there are libraries based on this pattern, with varying successes allowing the pattern to be partially level with the listed disadvantages. I would point out XCoordinator and RxFlow.

What have we done?

Having played with a project that we got from another team for support and development — with the coordinators and their simplified “great-grandmother” Router-s in the architectural approach VIPER. We rolled back to the approach that worked well previously in another large project of our company. This approach does not have any name. It basically lies on the surface. When we had free time, we isolated it into a separate library RouteComposer. It completely replaced our coordinators and proved to be more flexible.

What is this approach? The approach is relying on the stack (tree) of the view controllers as it is. Not creating an extra entity that needs to be monitored. And not saving or tracking states of the app.

Let’s look at the UIKit entities more carefully and figure out what we have in the bottom line and what you can work with:

  1. Controller view stack is a kind of tree. There is a root view controller, which has child view controllers. View controllers presented modally are a special case of child view controllers as they are also tied to the presented view controller. This is all available out of the box.
  2. We need to create entities of view controllers. They all have different constructors, they can be created using Xib files or Storyboards, they have different input parameters. But they are united by the fact that they all need to be created. So the pattern Factory, which knows how to create the controller, will suit us. Each factory produces a view controller entity, it is easy to cover with exhaustive unit tests and it is not dependent on others.
  3. Let’s divide the view controllers into 2 classes:
    1. Just view the controllers.
    2.
    Container View Controllers.
    Container View Controllers are different from the simple ones in that they can contain child view controllers which are also containers or simple ones. These view controllers are available out of the box: UINavigationController, UITabBarController and so on, but there can be custom ones created by the user. If you abstract, you can find the following properties in all containers:
    1. They have a list of all controllers that they contain.
    2. One or more controllers are currently visible.
    3. They may be asked to make one of these controllers visible.

    That is the only difference of the container view controllers in UIKit. They just have different methods to reach these three goals.
  4. To embed a view controller created by said factory, the parent view controller’s method is used: UINavigationController.pushViewController (…), UITabBarController.selectedViewController = …, UIViewController.present (…) .
    You may notice that you always need two view controllers for that process: The view controller that is already in the stack (tree), and one that needs to be built into the stack. Let’s wrap them into a wrapper and call it an Action. Each action is easy to cover with exhaustive unit tests and each is independent of the others.
  5. From the above, it turns out that by using these entities we can build a chain of configuration like Factory Action Factory Action Factory and, having run it, you can build a tree with the view of any complexity. We only need to specify the starting point. These starting points are usually either rootViewController belonging to UIWindow or the current view controller, which is the highest leaf of the tree. That is, this configuration is correctly written as:
    Starting ViewController Action Factory Factory.
  6. In addition, we will need an entity that knows how to run and build the provided configuration chain. Let’s call it Router. The router does not possess a state nor does it hold any references. It has one method to which the configuration is passed and it sequentially performs the configuration steps.
  7. Let’s add some extra responsibility to the configuration by adding interceptors to the chain. We will need the interceptors of three types: 1. Launched before the start of navigation. Here we can put the tasks of user authentication to the system and other asynchronous logic. 2. Executed at the moment of creating a controller view to set some values into it. 3. Performed after navigation, here you can put various analytical tasks. Each entity is easily covered by unit tests and does not know how it will be used in the configuration. It has only one responsibility and fulfils it. The configuration for complex navigation may look like:
    [Pre-navigation Task …] Starting ViewController Action (Factory + [ContextTask …]) (Factory + [ContextTask …]) [ Post Navigation Task …].
    That is, all tasks will be executed by the router sequentially, performing small, easily readable, atomic entities.
  8. There still remains the last task that is not solved by the configuration — that is the state of the application at the moment when the navigation should start. What if we do not need to build the entire chain of view controllers, but only part of it? This might happen because the user has already partially built it manually. This question can always be answered unambiguously by a tree with a view of controllers. If part of the chain is already built, it is already in the tree.
    So if each factory in the chain can answer the question whether it is built or not, then the router will be able to understand which part of the chain needs to be completed. Of course this is not a factory task, so another atomic entity is entered — Finder.
    Then any configuration looks like this:
    [Pre-navigation Task …] Starting ViewController Action (Finder / Factory + [ContextTask …]) (Finder / Factory + [ContextTask …]) [Post Navigation Task …].
    If the router starts reading the configuration backwards, then one of the finders will tell the router that the corresponding factory has already been built, and the router will begin to build the chain from this point forward. If none one of them finds the corresponding view controller in the tree, then it is necessary to build the whole chain starting with the initial controller.
The way Router runs the configuration chain

Lastly, we want the configuration to be strictly typed. Therefore each entity works with only one type of controller view; one type of data. Configuration completely relies on the ability of Swift to work with associatedtypes. We want to trust the compiler, not the runtime. The developer may intentionally weaken the typing, but not vice versa.

An example of this configuration:

Present Product View Controller modally inside of the UINavigationController from the topmost View Controller

The items described above cover the entire library and describe the approach. All that remains is to provide configurations for the chains that the router will perform when the user taps a button or an external event occurs. If these are different types of devices, for example, iPhone or iPad, then we can provide different configurations using polymorphism. If we have an A / B testing, we can do the same thing. We do not need to think about the state of the application at the time of the start of navigation, we just need to make sure that the configuration was initially written correctly, and we are sure that the router will build it one way or another.

The described approach is more complicated than a certain abstraction or pattern, but we have not yet encountered a task where it would not be enough. Of course, RouteComposer requires some study and understanding of how it works. However, this is much easier than learning the basics of AutoLayout or RunLoop.

The library, as well as the implementation of the router provided, does not use any tweaks of the Objective C runtime and fully follows all the concepts of UIKit. It only helps to break the composition process into steps and performs them in a given sequence. Library tested with iOS versions 9 through 13.

Thank you. I am happy to answer any questions you may have.

PS: If you like the library, do not forget to give it a star on GitHub!

Testimonials

Viz.ai

At Viz.ai, the leading synchronised stroke care service, we went into replacing our entire navigation system, and we knew we needed to address complex and dynamic navigation scenarios. Coordinators and other flow-control libraries just didn’t answer our needs, and lead to mixing application logic and navigation, or creating massive coordinator classes. RouteComposer was an amazing fit for us, and actually, as the creator of this library states, it is the drop in replacement for any coordinator code you currently use.

The separation of concerns on this library is absolutely beautiful, and as with anything genius, it all works like magic. It does have a small learning curve, but one that pays off far more than coordinators and flow controllers, and will save you a ton of coding once you implement it.

It makes navigation in the app as simple as saying “go to x with y” and not worrying about the current state or stack. I wholeheartedly recommend it.

Elazar Yifrach, Sr iOS Developer @ Viz.ai

Hudson’s Bay Company

In our iOS app we wanted to provide a seamless experience for our users to guarantee that whenever they click on a push notification or a link in an email, they will land on the required view in the app seamlessly no matter of the state of the app.

We tried a programmatic navigational approach in the code and also tried to rely on a few other libraries. However, none of them seemed to do the trick. RouteComposer was not our first choice as originally it looked too complex. Thankfully, it turned out to be a fantastic and elegant solution. We started to use it not only to handle external deeplinking but also to handle our internal navigation within the app.It also turned out to be a great tool for UI A/B tests when you have different navigation patterns for different users. It saved us a load of time, and we really like the logic behind it.

The creator of the library is super responsive and helped with all questions that we had. I would thoroughly recommend it!

Alexandra Mikhailouskaya, Senior Lead Engineer @ Hudson’s Bay Company

B.W.A.

We recently performed our fifth and largest app update which involved restructuring the user navigation from scratch. We started with a simple migration of our existing (six-file long) coordinator before one of our senior devs suggested we trial RouteComposer. The proof of concept was challenging, but Eugene put himself at my disposal to work through retrofitting the RouteComposer into our existing enterprise-grade codebase and when the pieces all fell into place, the result was simplicity itself.

Our other devs have embraced the RouteComposer in lieu of segues, unwind segues, manual pushes, pops, and modal drops and the resulting navigtion around our app is delightful.

Great thanks to Eugene for all his help.

skooter Martin, Senior Specialist Mobile Engineer @ B.W.A.

--

--