/ Miscellaneous

Unidirectional data flow with Combine

Unidirectional data flow with Combine

Recombine

Recombine is a Redux-like implementation of the unidirectional data flow architecture in Swift.

About Recombine

Recombine relies on three principles:

  • The Store stores your entire app state in the form of a single data structure. This state can only be modified by dispatching Actions to the store. Whenever the state in the store changes, the store will notify all observers.
  • Actions are a declarative way of describing a state change. Actions don't contain any code, they are consumed by the store and forwarded to reducers. Reducers will handle the actions by implementing a different state change for each action.
  • Reducers provide pure functions that create a new app state from actions and the current app state.

Recombine

For a very simple app, one that maintains a counter that can be increased and decreased, you can define the app state as following:

enum App {
    struct State {
      var counter: Int
    }
}

You would also define two actions, one for increasing and one for decreasing the counter. For the simple actions in this example we can use a very basic enum:

// It's recommended that you use enums for your actions to ensure a well typed implementation.
extension App {
    enum Action {
        case modify(Modification)

        enum Modification {
            case increase
            case decrease
        }
    }
}

Your reducer needs to respond to these different actions, that can be done by switching over the value of action:

extension App {
    let reducer = Reducer<State, Action> { state, action in
        switch action {
        case .modify(.increase):
            state.counter += 1
        case .modify(.decrease):
            state.counter -= 1
        }
    }
}

A single Reducer should only deal with a single field of the state struct. You can nest multiple reducers within your main reducer to provide separation of concerns.

In order to have a predictable app state, it is important that the reducer is always free of side effects, it receives the current app state and an action and returns the new app state.

To maintain our state and delegate the actions to the reducers, we need a store. Let's call it App.store:

extension App {
    static let store = Store<State, Action>(
        state: .init(counter: 0),
        reducer: reducer
    )
}

Now let's inject the store as an environment variable so that any views in our hierarchy can access it and automatically be updated when state changes:

// In SceneDelegate.swift.
window.rootViewController = UIHostingController(
    rootView: ContentView().environmentObject(App.store)
)

Now it can be accessed from any of our views!

@EnvironmentObject var store: Store<App.State, App.Action>

GitHub