Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
There’s another thing we can do to improve the ergonomics of our architecture, and that’s in the view layer. Right now, a view holds onto a view store that contains all of the state it cares about to render itself, and in order to access this state, we dive through the view store’s value
property.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Let’s add a little more polish to the Composable Architecture. Right now there is no easy way of working with optional state. In particular, it is not possible to write a reducer on non-optional state and use
pullback
to transform it to a reducer that works on optional state.Define a custom property on
Reducer
that transforms reducers on non-optional state to reducers on optional state.Solution
extension Reducer { public var optional: Reducer<Value?, Action, Environment> { .init { value, action, environment in guard value != nil else { return [] } return self(&value!, action, environment) } } }
There is also no easy way of working with collections in state. In particular, it is not possible to write a reducer on an element of state and use
pullback
to transform it to a reducer that works on a collection of state.Define an
indexed
method onReducer
that handles this kind of transformation such that the state’s key path is of the formWritableKeyPath<GlobalValue, [Value]>
. In order to send an action to a particular element of the array, it must identify the element in some way. Take inspiration from the method’s name. 😁Solution
Given some global app state:
struct AppState { var list: [RowState] }
In order to send actions to individual elements, you can identify them by index.
enum AppAction { case list(index: Int, action: RowAction) }
Which means that
indexed
would take a case path fromAppAction
to(Int, Action)
.From this we can deduce the signature and define the following method:
extension Reducer { func indexed<GlobalValue, GlobalAction, GlobalEnvironment>( value: WritableKeyPath<GlobalValue, [Value]>, action: CasePath<GlobalAction, (Int, Action)>, environment: @escaping (GlobalEnvironment) -> Environment ) -> Reducer<GlobalValue, GlobalAction, GlobalEnvironment> { .init { globalValue, globalAction, globalEnvironment in guard let (index, localAction) = action.extract(from: globalAction) else { return [] } return self( &globalValue[keyPath: value][index], localAction, environment(globalEnvironment) ) .map { effect in effect .map { action.embed((index, $0)) } .eraseToEffect() } } } }