|
| 1 | +extension Reducer { |
| 2 | + /** |
| 3 | + A type-lifting method. The global state of your app is _Whole_, and the `Reducer` handles _Part_, that is a |
| 4 | + sub-state. |
| 5 | + |
| 6 | + Let's suppose you may want to have a `gpsReducer` that knows about the following `struct`: |
| 7 | + ``` |
| 8 | + struct Location { |
| 9 | + let latitude: Double |
| 10 | + let longitude: Double |
| 11 | + } |
| 12 | + ``` |
| 13 | + |
| 14 | + Let's call it _Part_. Both, this state and its reducer will be part of an external framework, used by dozens of |
| 15 | + apps. Internally probably the `Reducer` will receive some known `ActionType` and calculate a new location. On the |
| 16 | + main app we have a global state, that we now call _Whole_. |
| 17 | + |
| 18 | + ``` |
| 19 | + struct MyGlobalState { |
| 20 | + let title: String? |
| 21 | + let listOfItems: [Item] |
| 22 | + let currentLocation: Location |
| 23 | + } |
| 24 | + ``` |
| 25 | + |
| 26 | + As expected, _Part_ (`Location`) is a property of _Whole_ (`MyGlobalState`). This relationship could be less |
| 27 | + direct, for example there could be several levels of properties until you find the _Part_ in the _Whole_, like |
| 28 | + `global.firstLevel.secondLevel.currentLocation`, but let's keep it a single-level for this example. |
| 29 | + |
| 30 | + Because our `Store` understands _Whole_ (`MyGlobalState`) and our `gpsReducer` understands _Part_ (`Location`), we |
| 31 | + must `lift` the `Reducer` to the _Whole_ level, by using: |
| 32 | + |
| 33 | + ``` |
| 34 | + let globalStateReducer = gpsReducer.lift( |
| 35 | + actionGetter: { $0 }, |
| 36 | + stateGetter: { global in global.currentLocation }, |
| 37 | + stateSetter: { global, part in global.currentLocation = path } |
| 38 | + ) |
| 39 | + // where: |
| 40 | + // globalStateReducer: Reducer<MyAction, MyGlobalState> |
| 41 | + // ↑ lift |
| 42 | + // gpsReducer: Reducer<MyAction, Location> |
| 43 | + ``` |
| 44 | + |
| 45 | + Now this reducer can be used within our `Store` or even composed with others. It also can be used in other apps as |
| 46 | + long as we have a way to lift it to the world of _Whole_. |
| 47 | + |
| 48 | + Same strategy works for the `action`, as you can guess by the `actionGetter` parameter. You can provide a function |
| 49 | + that takes a global action (_Whole_) and returns an optional local action (_Part_). It's optional because perhaps |
| 50 | + you want to ignore actions that are not relevant for this reducer. |
| 51 | + |
| 52 | + - Parameters: |
| 53 | + - actionGetter: a way to convert a global action into a local action, but it's optional because maybe this |
| 54 | + reducer shouldn't care about certain actions. Because actions are usually enums, you can switch |
| 55 | + over the enum and in case it's nothing you care about, you simply return nil in the closure. If |
| 56 | + you don't want to lift this reducer in terms of `action`, just provide the identity function |
| 57 | + `{ $0 }` as input. |
| 58 | + - stateGetter: a way to read from a global state and extract only the part that it's relevant for this reducer, |
| 59 | + by traversing the tree of the global state until you find the property you want, for example: |
| 60 | + `{ $0.currentGame.scoreBoard }` |
| 61 | + - stateSetter: a way to write back into the global state once you finished reducing the _Part_, so now you have |
| 62 | + a new part that was calculated by this reducer and you want to set it into the global state, also |
| 63 | + provided as the first parameter as an `inout` property: |
| 64 | + `{ globalState, newScoreBoard in globalState.currentGame.scoreBoard = newScoreBoard }` |
| 65 | + - Returns: a `Reducer<GlobalAction, GlobalState>` that maps actions and states from the original specialised |
| 66 | + reducer into a more generic and global reducer, to be used in a larger context. |
| 67 | + */ |
| 68 | + public func lift<GlobalActionType, GlobalStateType>( |
| 69 | + actionGetter: @escaping (GlobalActionType) -> ActionType?, |
| 70 | + stateGetter: @escaping (GlobalStateType) -> StateType, |
| 71 | + stateSetter: @escaping (inout GlobalStateType, StateType) -> Void) |
| 72 | + -> Reducer<GlobalActionType, GlobalStateType> { |
| 73 | + .reduce { globalAction, globalState in |
| 74 | + guard let localAction = actionGetter(globalAction) else { return } |
| 75 | + var localState = stateGetter(globalState) |
| 76 | + self.reduce(localAction, &localState) |
| 77 | + stateSetter(&globalState, localState) |
| 78 | + } |
| 79 | + } |
| 80 | +} |
0 commit comments