Craft At WillowTree Logo
Content for craftspeople. By the craftspeople at WillowTree.
Engineering

App Coordinators and Redux on iOS

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:

  • App State: All the information necessary to render the UI at a single point in time.
  • Action: An instance of a value type that represents the data necessary to change the app state in a particular way.
  • Reducer: A method that combines an app state and an action into a new app state.
  • Store: The object that stores the app state and sends actions to the reducer, updating the app state.
  • Dispatch: The act of sending an action to the store.

Redux Problems

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.

Async Work

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:

  1. Action Creators: Some type should capture the app store, perform the asynchronous work, and dispatch additional actions. See this thread for a detailed walkthrough of this approach.
  2. Middleware: Middleware should observe relevant actions and dispatch additional actions asynchronously. See redux-thunk and redux-saga.
  3. Reducers: Customized reducers should reduce actions not just to a state, but a (state, command) tuple that includes instructions for asynchronous work. The store should then execute commands and dispatch further actions. See redux-loop.

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?

User Actions vs Data Actions

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.

Routing with UIKit

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

I didn’t invent them.

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:

  1. Instantiate view controllers
  2. Handle their user actions
  3. Perform navigation between them

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.

A Perfect Match

By combining app coordinators and Redux, I think we can reduce our view controllers to exactly two responsibilities:

  1. Render relevant app state
  2. Convert UI interactions into user actions

They can look like this:

struct ProductViewModel {
    let name: String
    let sku: String
    let price: Double
}


protocol ProductViewControllerHandler: class {
    func purchase(sku: String)
}


final class ProductViewController: UIViewController {
    @IBOutlet var nameLabel: UILabel!


    weak var handler: ProductViewControllerHandler!
    var viewModel: ProductViewModel!


    func inject(handler handler: ProductViewControllerHandler) {
        self.handler = handler
    }


    func render(viewModel: ProductViewModel) {
        self.viewModel = viewModel
        nameLabel.text = viewModel.name
    }


    @IBAction func purchaseButtonTapped(sender: AnyObject) {
        handler.purchase(viewModel.sku)
    }
}

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.

Cordux App Coordinators

With Cordux, app coordinator responsibilities have clear implementations:

  1. Instantiate view controllers and subscribe them to the store
  2. Handle their user actions_ by dispatching data actions to the store_
  3. Perform navigation between them via the app state’s route

Subscribing to the Store

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:

extension SignInViewModel {
    init(_ state: AppState) {
        username = state.authentication.username
        password = state.authentication.password
        usernameIsValid = username.length > 8
    }
}


store.subscribe(signInViewController, SignInViewModel.init)

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.

struct AppState {
    var products: [String:Product]
    // ...
}


extension ProductViewModel {
    static func create(sku: String) -> (AppState) -> (ProductViewModel) {
        return { state in
            return ProductViewModel(product: state.products[sku]!)
        }
    }
}


store.subscribe(productViewController, ProductViewModel.create(id))

Dispatching Actions to the Store

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.

Routing via App State

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):

public protocol StateType {
    var route: Route { get set }
}


public final class Store<State : StateType> {
    var state: State


    public func dispatch(action: Action) {
        state = reducer.handleAction(action, state: state)
        subscriptions.forEach { $0.notify(state) }
    }


    public func route(action: RouteAction) {
        state.route = reduceRoute(action, route: state.route)
        dispatch(action)
    }


    public func setRoute(action: RouteAction) {
        state.route = reduceRoute(action, route: state.route)
    }


    // ...
}

With these helpers, our AuthenticationAppCoordinator can show our forgot password screen like so:

func forgotPassword() {
    store.route(.push("forgot-password"))
}

And then update the route directly when that controller is popped off like this:

@objc func didMove(toParentViewController parentViewController: UIViewController?,
                                          viewController: UIViewController) {
    if parentViewController == nil {
        store.setRoute(.pop("forgot-password"))
    }
}

Cordux

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.

Table of Contents
Ian Terrell

Read the Video Transcript

Recent Articles