|
| 1 | +extension ReducerProtocol { |
| 2 | + /// Adds a reducer to run when this reducer changes the given value in state. |
| 3 | + /// |
| 4 | + /// Use this operator to trigger additional logic when a value changes, like when a |
| 5 | + /// ``BindingReducer`` makes a deeper change to a struct held in ``BindingState``. |
| 6 | + /// |
| 7 | + /// ```swift |
| 8 | + /// struct Settings: ReducerProtocol { |
| 9 | + /// struct State { |
| 10 | + /// @BindingState var userSettings: UserSettings |
| 11 | + /// // ... |
| 12 | + /// } |
| 13 | + /// |
| 14 | + /// enum Action: BindableAction { |
| 15 | + /// case binding(BindingAction<State>) |
| 16 | + /// // ... |
| 17 | + /// } |
| 18 | + /// |
| 19 | + /// var body: some ReducerProtocol<State, Action> { |
| 20 | + /// BindingReducer() |
| 21 | + /// .onChange(of: \.userSettings.isHapticFeedbackEnabled) { oldValue, newValue in |
| 22 | + /// Reduce { state, action in |
| 23 | + /// .run { send in |
| 24 | + /// // Persist new value... |
| 25 | + /// } |
| 26 | + /// } |
| 27 | + /// } |
| 28 | + /// } |
| 29 | + /// } |
| 30 | + /// ``` |
| 31 | + /// |
| 32 | + /// When the value changes, the new version of the closure will be called, so any captured values |
| 33 | + /// will have their values from the time that the observed value has its new value. The system |
| 34 | + /// passes the old and new observed values into the closure. |
| 35 | + /// |
| 36 | + /// > Note: Take care when applying `onChange(of:)` to a reducer, as it adds an equatable check |
| 37 | + /// > for every action fed into it. Prefer applying it to leaf nodes, like ``BindingReducer``, |
| 38 | + /// > against values that are quick to equate. |
| 39 | + /// |
| 40 | + /// - Parameters: |
| 41 | + /// - toValue: A closure that returns a value from the given state. |
| 42 | + /// - reducer: A reducer builder closure to run when the value changes. |
| 43 | + /// - oldValue: The old value that failed the comparison check. |
| 44 | + /// - newValue: The new value that failed the comparison check. |
| 45 | + /// - Returns: A reducer that performs the |
| 46 | + @inlinable |
| 47 | + public func onChange<V: Equatable, R: ReducerProtocol>( |
| 48 | + of toValue: @escaping (State) -> V, |
| 49 | + @ReducerBuilder<State, Action> _ reducer: @escaping (_ oldValue: V, _ newValue: V) -> R |
| 50 | + ) -> _OnChangeReducer<Self, V, R> { |
| 51 | + _OnChangeReducer(base: self, toValue: toValue, reducer: reducer) |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +public struct _OnChangeReducer<Base: ReducerProtocol, Value: Equatable, Body: ReducerProtocol>: |
| 56 | + ReducerProtocol |
| 57 | +where Base.State == Body.State, Base.Action == Body.Action { |
| 58 | + @usableFromInline |
| 59 | + let base: Base |
| 60 | + |
| 61 | + @usableFromInline |
| 62 | + let toValue: (Base.State) -> Value |
| 63 | + |
| 64 | + @usableFromInline |
| 65 | + let reducer: (Value, Value) -> Body |
| 66 | + |
| 67 | + @usableFromInline |
| 68 | + init( |
| 69 | + base: Base, |
| 70 | + toValue: @escaping (Base.State) -> Value, |
| 71 | + reducer: @escaping (Value, Value) -> Body |
| 72 | + ) { |
| 73 | + self.base = base |
| 74 | + self.toValue = toValue |
| 75 | + self.reducer = reducer |
| 76 | + } |
| 77 | + |
| 78 | + @inlinable |
| 79 | + public func reduce(into state: inout Base.State, action: Base.Action) -> EffectTask<Base.Action> { |
| 80 | + let oldValue = toValue(state) |
| 81 | + let baseEffects = self.base.reduce(into: &state, action: action) |
| 82 | + let newValue = toValue(state) |
| 83 | + return oldValue == newValue |
| 84 | + ? baseEffects |
| 85 | + : .merge(baseEffects, self.reducer(oldValue, newValue).reduce(into: &state, action: action)) |
| 86 | + } |
| 87 | +} |
0 commit comments