Introducing an iOS navigation library based on the coordinator pattern
We at QuickBird Studios are working on large-scale apps that grow over years and that’s why our team worked really hard to find suitable architectural patterns for our use cases. We described our use of MVVM and Functional Reactive Programming in some of our previous blog posts like this one.
One topic we didn’t talk about yet is how we control the flow of scenes so that we can reuse them in different parts of our apps. Naturally, views tend to become overstuffed and get too many responsibilities. Today we show you a solution for separating the navigation logic from your exchangeable view components. Actually, it seems to be one of the more challenging problems to solve as an app programmer. Therefore, we developed a library called XCoordinator to solve this which we’ll describe in this article. But before we talk about the solution let’s take a look at the problems we face if we do it the standard way.
What’s the actual problem?
Most app developers control the flow of screens in their apps in their view components (e.g. the ViewController). Even Apple and Googles sample code promotes this simple solution. This approach may be fine until you want to reuse one of those views in a different context.
With this approach, the views need to have knowledge of the context in which they are used and that makes them less reusable and extremely large and hard to manage. Developers end up in situations where they write code like this:
The coordinator pattern
One easy to set up solution for this problem is to remove the responsibility of managing the flow of screens from the view components and move it to a separate higher-level layer. This pattern is named coordinator pattern and is described in some really nice articles by Soroush Khanlou here and here.
We started to use the coordinator pattern for our apps and it allowed us to create reusable views that are easier to test and allowed us to extract the ViewModel and View initialization to a separate layer. Especially the creation and configuration of those components can be really complex and putting this logic in other views (or view models) makes them really coupled.
The coordinator can be called from different components. If you are using the MVC design architecture you will probably trigger the coordinator in your view components, if you are using MVVM you can call it either from the Views or the ViewModels. The guys from objc.io describe in their app architecture book the approach of calling it from the view components. We at QuickBird Studios prefer to do it from the ViewModels.
The coordinator pattern doesn’t necessarily mean you have to use the MVVM architecture for your apps, but it fits really nice with the idea of separating concerns of your components. The combination of MVVM and the coordinator pattern is well known under the name of MVVM-C.
Taking coordinators to the next level
In the last chapter of Souroush’s article, he mentions that there is actually no need for a library for using the coordinator pattern:
Ultimately, coordinators are just an organizational pattern. There’s no library you can use for coordinators because they’re so simple.
He is principally right but after some time we saw that coordinators share a lot of code and they were mostly handling the routing of screens and that’s when we started to introduce a new terminology: router and coordinator
A router is an object that knows how to navigate to new screens under different circumstances and abstracts platform specific navigation code away. If we take iOS as an example the router would know how to push views to a Navigation Controller or how to present views modally.
A coordinator is much more than a router. It does route between screens, yes, but it is also responsible to decide which route to take after a specific action happened, it creates and configures Views and ViewModels and ultimately knows how to connect the routes to create a flow. Sometimes your Coordinator is also the perfect place to inject dependencies into your components.
Combining the idea of a Router and a Coordinator
After collecting all of our ideas about what our coordinator objects should do, we started to realize that there were a lot of things that could be abstracted away.
Our coordinator shouldn’t need to repeat the routing code over and over again and that’s why our default coordinator implementation provides you with all the routing code needed. Additionally, we should be able to replace and inject different coordinators for different use cases. And last but not least we want to be able to use custom animations/transitions to navigate to our screens.
That was the moment when XCoordinator was born. We combined all of our ideas and wrote our own framework. We used and improved it over the following weeks and months and then released and open sourced it.
To get started using it you just have to create an enum with all of the navigation paths for a particular flow :
and to trigger the transition from your View or ViewModel:
If you want to learn more about how to use XCoordinator take a look at our Github repo.
Our framework abstracts the routing responsibility away and provides you with a frame for implementing your own coordinators. It’s especially useful for implementing MVVM-C, Model-View-ViewModel-Coordinator.
XCoordinator proposes you to trigger the routing from your ViewModels were your actual view logic should be. This removes redundant communication between View and ViewModel.
To create a type-safe interface we use routes to define the actual navigation paths from a view to another one. The coordinators then are handling those routes and perform the actual navigation to this scene.
Transitions are then describing the transition between two views and since we know that standard transitions are not the only transitions we use in our apps we added complete support for custom view transitions.
Introducing all these new components may seem like an overhead, but you can actually mix transitions, routes and coordinators to create new combinations. That makes it super simple to add new routes and new transitions without changing your logic in your Views or ViewModels.
Conclusion
Introducing the coordinator pattern helped as tremendously to remove even more responsibilities from the view components and helped us to write more reusable and better testable views. By adopting the pattern for our use cases and writing our own framework we came to a solution that fits really nice in our app architecture.
We were amazed by the feedback we got for our framework and tried to include as many suggestions as possible. By introducing new types of components and settings type safety as our main goal we ended up with a redesigned framework we called XCoordinator. It’s freely available as open-source software on GitHub.
If you’re using SwiftUI instead of UIKit, you will need to tweak the Coordinator pattern a little, due to a changed paradigm and the way SwiftUI works. But worry not – we got you covered with another article on how to do this: How to Use the Coordinator Pattern in SwiftUI.
Stay tuned! 😎