This repository demonstrates a pattern of asynchronous state management based on ideas of reactive programming, state reducer and unidirectional data flow. This is not a library, because you don’t need yet another library to implement this.
- Install Carthage
brew install carthage
- Build the dependencies
carthage bootstrap --platform iOS
Asynchronous state management is a non-trivial modelling exercise and requires a reliable approach to keep it concise and maintainable.
Most trivial applications start without any explicit state management approach. However, things quickly get out of hand when the number of states in which a system can reside starts to grow.
Finite-State Machine and State Pattern can help manage synchronous state transitions, but are not designed to handle asynchronous behaviour.
Unless you can model your entire system synchronously, a single asynchronous source breaks imperative programming.
-- Jake Wharton
Explicitly define the state of a system, then use a reducer function to compute a new state based on the previous state. Use reactive streams to handle asynchronous tasks and make data flow in one direction.
- An event is received and transformed to an
Action
. - The
Action
is handled and transformed toMutation
. This transformation is needed whenActions
require asynchronous handling. SuchActions
produce multipleMutations
(e.g., (1)loading
, (2)loaded
) - Each
Mutation
is passed to theReducer
function, which uses it to transform the currentState
to a newState
.
Where:
Action
- an interpretation of the system or user eventMutation
- a result of theAction
handlingState
- a model of the system or use caseReducer
- a pure function which takes aState
and aMutation
and produces a newState
View
receives a user event.View
transforms the event to anAction
.View
propagates theAction
toInteractor
.Interactor
handles theAction
and transforms it toMutations
.Interactor
uses eachMutation
to transform the currentState
to a newState
.Interactor
propagates the newState
toView
View
updates itself using the newState
. The unidirectional feedback loop is now complete.
Given that actions
is Observable<Action>
, states
is Observable<State>
, the unidirectional feedback loop is expressed as:
states = actions.flatMap(handleAction).scan(State.initial, accumulator: reduce)
Action
and Mutation
are expressed as an enum
:
enum Action {
case reload
}
enum Mutation {
case loading
case records([Record])
case failure(Error)
}
State
is expressed as a struct
(if there is a strict number of finite states, then enum
):
struct State {
var records: [Record]
var isLoading: Bool
var error: Error?
static var initial = State(records: [], isLoading: false, error: nil)
}
Reducer
is a pure function:
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case let records(records):
state.records = records
case let failure(error):
...
case loading:
...
}
return state
}
The function handleAction
is asynchronous:
func handleAction(_ action: Action) -> Observable<Mutation> {
switch action {
case reload:
return gateway.fetchRecords()
.map(Mutation.records)
.startWith(Mutation.loading)
.catchError({ .just(Mutation.failure($0)) })
}
}
Check branch state-feedback
To introspect State
during handleAction
it can be fed back into the loop:
func start(actions: Observable<Action>) -> Observable<State> {
let states = BehaviorRelay(value: State.initial)
return actions
.withLatestFrom(states, resultSelector: { ($0, $1) })
.flatMap(handleAction)
.scan(states.value, accumulator: reduce)
.do(onNext: states.accept)
}
The function handleAction
will now take two parameters:
func handleAction(_ action: Action, state: State) -> Observable<Mutation> {
...
}