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.
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>