Flow Coordinators in iOS
As developers we are always searching for, learning, and experimenting with different approaches to software development. I am always looking for ways to better manage complexity in my apps and to write more expressive and maintainable code. Recently I discovered several articles discussing an enterprise design pattern that is beginning to be applied in iOS development. This enterprise pattern is called the Application Controller pattern.
I first came across this concept in Soroush Khanlou’s blog post 8 Patterns to Help You Destroy Massive View Controllers where he mentions a design approach called the Navigator. This design concept encapsulates the presentation of view controllers into a separate object. Exploring his blog further I discovered a talk he gave at NSSpain entitled Presenting “Coordinators” in which he clarifies and expands on the original notion. In the talk he credits the origination of the design to a book by Martin Fowler called Patterns of Enterprise Application Architecture where the concept is formally titled the Application Controller pattern. Soroush is not the only iOS developer discussing this pattern of moving the responsibility of navigation outside of the view controller, several other articles can be found under the title Flow Controllers as discussed by Krzysztof Zablocki, Alberto Debortoli, and others.
Going forward I will refer to this concept as Flow Coordinators or just Coordinators for simplicity and consistency with Soroush. I feel the term controller already has a certain connotation in iOS and we want to avoid any undue confusion. The term Coordinator seems more fitting for the role of these objects.
We have all seen (and written) code that pushes or modally displays a new view controller from an existing view controller. In general, we create the new view controller, pass in any data that it might need, and then either present it directly (modal) or grab the navigation controller and tell it to “push” the new view controller.
let viewController = CustomViewController()
viewController.data = NSObject() //some data object
navigationController.show(viewController, sender: self)
This process is often repeated in a given user flow where each previous view controller configures the subsequent view controller. This presents several problems, such as a child object accessing a parent object directly, any data required by a down stream view controller needing to be passed through all previous view controllers, it makes testing more complicated, and if a flow needs to be used on a different device type we have to add logic checks. The Coordinator pattern helps to alleviate these issues and make your view controllers easier to reuse, allows for the reuse of user flows, and separates concerns among objects.
The basic concept is really quite simple, you move all the navigation specific UIKit calls into custom objects. These objects are responsible for performing the navigation for each specific flow or even sub-flow in an app. For example if you have a Signup flow in your app, then this would be handled by the Signup Coordinator. A forgot password flow, handled by the Forgot Password Coordinator. There is also an initial App Coordinator that would kick off the application flow and spawn new coordinators as required handling new user flows. Coordinators being able to spawn additional coordinators allows for easier reuse of both work flows and coordinators. A given coordinator is responsible for creating any required view controllers, view models, and responding to any actions via delegation.
Since the initial presentation did not include a code sample and the original article was a bit light on code as well (there is an app on GitHub that you can download and review) I decided to implement flow coordinators myself to better understand the potential benefits. Reviewing a myriad of posts on the topic only served to further complicate the issue in that different authors employ different techniques in their “Coordinators”. Some injected the flow coordinator into the base view controller of the flow, while others used protocols and delegation, still others used blocks/closures. Some authors only used the coordinator for navigation, others suggested the coordinators handle network or persistence access (keeping view controllers as display only objects). Since I wanted to get a basic understanding I only focused on navigation. In the end I decided to use the delegation approach as well since this more loosely couples the controllers and the coordinators, as well as defining a clear interface. It does however require some additional boilerplate code since each view controller needs to declare a protocol to delegate back to the appropriate flow controller.
In order to get a better feel for how flow coordinators would work in an actual app, I decided to create a simple iOS app that had a signup flow and a profile flow once the user was logged in. The profile flow simulates a tab bar controller with additional content. This is a contrived example for sure, but it gives us the opportunity to see how flow coordinators can handle navigation as well as custom navigation animation.
The Login button will take you directly to the Profile screen where we simulate several features via the pseudo tab bar. Settings will modally display a view controller and Following and Followers will use a custom navigation delegate to transition to those respective view controllers. The Signup button will navigate through a pair of simple screens to simulate user name, password selection, and account generation. For simplicity, no values are actually required, and nothing is actually processed or validated.
Overall setting up the flow coordinators is not that difficult. We begin by creating the App Coordinator in the App Delegate. Here we create a rootview controller, and initialize the App Coordinator with it, and then call start.
Our first coordinator is the App Coordinator. This coordinator checks the user’s login in status, and depending on the result either shows the login screen or the profile screen. Both of the screens and related flows are controlled by two child coordinators. The logic is pretty simple and easy to follow. The App Coordinator acts as the delegate for its child coordinators, and receives a response when the authentication process is completed, so that it can move to the next flow, the profile flow.
Next up is our Authentication Coordinator, which as the name implies would authenticate the user, or in our case it walks us through the pseudo signup process. In this example there are two screens presented in sequence to simulate choosing a user name, password, and generating the account. The Authentication Coordinator handles the display of each subsequent screen based on the Next or Create Account actions. In this flow we use a standard push navigation animation.
There is no need for a child coordinator property, since this flow does not generate any additional flows, it simply completes and notifies the parent (App Coordinator). If all goes as planned then we would move onto the Profile Coordinator. This flows simulates a basic start screen in an app with the ability to access additional content (Following and Followers as well as a Settings page). This flow uses a custom navigation delegate in order to use a custom animation when transitioning to the Following/Followers view controllers, while settings are displayed as a standard modal.
Again the logic and the implementation are pretty straight forward and easy to follow. Each action (Settings/Followers/Following) is handled by the profile coordinator to display the appropriate view controller. The custom animator and navigation delegate handle all the animation aspects. This keeps all the components small and easy to understand.
Since we are using delegation we use reference types for our coordinators. Implementing delegation in Swift requires defining a weak delegate variable, and the weak modifier is only available to class protocols. I created an empty base class, Coordinator, to simplify storing child coordinators in a typed array. We could move the common attributes like the child coordinators property and the navigation delegate into the super class in a production app.
Moving the navigation flow into separate objects feels counter intuitive at first, but once you start working with flow coordinators you quickly come to realize the benefits. Flows are well contained and separated from the view controllers and each other. You can easily change the animations for a given flow by substituting a different navigation delegate for the flow or inject a different animator into the navigation delegate. View controllers now know nothing about the flow of the app, and no longer have the responsibility of presenting or dismissing content. Less responsibility is good, we have made our view controllers a little more clean and easier to reuse.
Soroush suggested that the coordinators handle model mutation as well to keep the view controllers as “display only” objects. I like using MVVM and RxSwift in my apps. If we were using MVVM we might consider making the view model the view controller’s delegate, and then forward all the navigation actions through the view model onto the coordinator. This would depend on your implementation and if there were any actions that the view model might need to perform prior to a given navigation. In MVVM it is not uncommon for the view model to be responsible for handling requests for model mutation, if not mutating the model directly.
Flow Coordinators look like another potentially useful tool in alleviating the massive view controller issue so common in iOS apps. I look forward to trying them out in a full project in the near future.