Over the spring and summer I’ve been exploring how to best leverage unidirectional data flow in iOS apps. It looks like the pattern will bring some huge wins in terms of simplifying our apps’ UI tiers. I’ve been looking mostly at using a Redux-like implementation.
For an introduction to the concepts of unidirectional data flow on iOS, I recommend this talk by Benjamin Encz, the main author of ReSwift , a Redux-like library in Swift. This article uses terms from Redux and ReSwift. Here’s the glossary:
I do believe that a Redux-like architecture will do wonders for our UI, but it’s not a perfect, all-encompassing paradigm on its own. It has a few flaws, especially when brought to iOS.
In my opinion, the core of Redux is reducing an action and an app state to a new app state. The architecture has no opinion about how those actions should be created. This is immediately evident when you look at asynchronous tasks and try to decide how actions should come out of them. There are at least three opinions on where asynchronous work and side effects should occur:
I believe the best answer is action creators. When working with Redux and React on the web they have a clear home (they can be bound to component props). But where should they live on iOS?
In any app there are actions that a user takes to manipulate the UI. These are related to but are distinct from the actions that reducers use to update the state. I’ve been calling the latter “data actions” since they are both made up of data and result in an update to data.
In Redux apps, I think these two concepts can become conflated. It is a form of tight coupling to express a low-level user interaction (a touch, a swipe) directly as a data action (UPDATE_ROUTE(PUSH_FORGOT_PASSWORD)). I believe it is better to bridge the low-level interactions to higher level user actions or intents (forgotPasswordTapped()). This means using action creators for all actions, even trivial and synchronous ones.
The web has it easy. They have one window and one DOM. On iOS we are mostly tied to UIKit. This makes routing via a Redux-managed state a non-trivial problem to solve.
First, who should manipulate the navigation hierarchy? The “standard way” is for view controllers to manually show other view controllers, either modally or by leveraging a parent navigation controller. How does this fit into route updates on a state?
Second, how do we interact with the core UIKit classes to keep our state up to date? While some have delegates that will report the relevant actions (like tapping a tab on a UITabBarController), others do not (like popping a view controller off of a UINavigationController).
App coordinators are a solution to the “massive view controller” problem. App coordinators take over many roles normally associated with the view controller in order to allow it to stay focused on just the UI components of the app.
The central responsibilities of an app coordinator are:
In this world, the view controller renders and manages the view, and delegates all other activities to the app coordinator via its delegate. The app coordinator conforms to the delegate, receives those user actions, and handles updating state via the database, network access, etc. Note that the view controller doesn’t even need to know about the app coordinator specifically, it only needs to know about its own delegate type.
This is a great pattern, and is useful to adopt even in apps that are not also using a Redux-like data flow. But it turns out they’re amazing when you put them together.
By combining app coordinators and Redux, I think we can reduce our view controllers to exactly two responsibilities:
They can look like this:
Our simple view controllers are now focused entirely on the UI. They have no non-UI logic, no dependencies other than a handler, and they don’t care about navigation. They don’t know about Redux or care about how they receive their view model to render. You could use the same exact code with an MVVM approach or something else entirely.
I’m calling combining app coordinators and a Redux flow Cordux.
With Cordux, app coordinator responsibilities have clear implementations:
With Cordux, the app coordinator is responsible for subscribing a view controller to the store, and for making sure it only sees the subset of app data that it cares about. The subscription itself should translate the app state to the final view model, including deriving any view-specific data.
We could do it something like this:
Here, the last parameter to subscribe is a function that translates the app state into a more specific model. This signature is taken directly from ReSwift.
We also need to be able to capture data that may be necessary to create the view model, because we often need a way to display multiple different instances of the same type. For instance, our ProductViewController shows product data for any product, and we may have a navigation stack of multiple ProductViewControllers each showing different products. We can solve this by currying our identifying information.
By removing any direct concept of Redux from the view controller, we’ve already separated our user actions from our data actions. And we’ve answered where our action creators should live.
Our app coordinator conforms to the view controller’s handler (or manages an instance of another conforming type, to avoid the potential “massive app coordinator” problem). Those handler methods are our action creators, and they are free to perform synchronous or asynchronous work as necessary to dispatch the appropriate data actions.
This provides a clear division of responsibilities. The view controller converts interaction events to user actions, and the app coordinator converts the user actions to data actions.
The challenges that Redux-like architectures face on iOS with routing are mitigated by app coordinators. Firstly, they are a clear place to handle all navigation, including routing. Secondly, they’re clear contenders for keeping up with route-related state changes that do not go through the store, such as a user popping a view controller off of a UINavigationController.
It is simplest if app coordinators implement all navigation via a Route stored inside the app state. This means that normal navigation (push a view controller) and “routing” (set a route in app state and expect the UI to update appropriately) are both performed by the exact same code path. As a non-trivial bonus, deep linking into the app is reduced to a problem of turning a URL into a Route.
To manage the route properly, I’ve been experimenting with additional support in the store itself. This includes special methods to either dispatch a routing action or update the state directly without invoking reducers and notifying observers. Something like this (simplified):
With these helpers, our AuthenticationAppCoordinator can show our forgot password screen like so:
And then update the route directly when that controller is popped off like this:
App coordinators are useful on their own, even if you don’t use a Redux-like architecture. I recommend you read this blog post and learn about them in general. Redux-like architectures are also useful in general. I recommend you check out ReSwift and see if it could improve your UI.
But I think they’re best put together.
I’m experimenting with building out framework support to concretize the architecture and share solutions for common issues and code for common app coordinators. The subscription and store code is based on ReSwift which has my great appreciation.
See our example app and keep track of progress at willowtreeapps/cordux.