|
78 | 78 | }
|
79 | 79 | }
|
80 | 80 |
|
| 81 | + private final class BindableActionDebugger<Action>: Sendable { |
| 82 | + let isInvalidated: @MainActor @Sendable () -> Bool |
| 83 | + let value: any Sendable |
| 84 | + let wasCalled = LockIsolated(false) |
| 85 | + init( |
| 86 | + value: some Sendable, |
| 87 | + isInvalidated: @escaping @MainActor @Sendable () -> Bool |
| 88 | + ) { |
| 89 | + self.value = value |
| 90 | + self.isInvalidated = isInvalidated |
| 91 | + } |
| 92 | + deinit { |
| 93 | + let isInvalidated = mainActorNow(execute: isInvalidated) |
| 94 | + guard !isInvalidated else { return } |
| 95 | + guard wasCalled.value else { |
| 96 | + var valueDump: String { |
| 97 | + var valueDump = "" |
| 98 | + customDump(self.value, to: &valueDump, maxDepth: 0) |
| 99 | + return valueDump |
| 100 | + } |
| 101 | + reportIssue( |
| 102 | + """ |
| 103 | + A binding action sent from a store was not handled. … |
| 104 | +
|
| 105 | + Action: |
| 106 | + \(typeName(Action.self)).binding(.set(_, \(valueDump))) |
| 107 | +
|
| 108 | + To fix this, invoke "BindingReducer()" from your feature reducer's "body". |
| 109 | + """ |
| 110 | + ) |
| 111 | + return |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | + |
81 | 116 | extension BindableAction where State: ObservableState {
|
| 117 | + fileprivate static func set<Value: Equatable & Sendable>( |
| 118 | + _ keyPath: _WritableKeyPath<State, Value>, |
| 119 | + _ value: Value, |
| 120 | + isInvalidated: (@MainActor @Sendable () -> Bool)? |
| 121 | + ) -> Self { |
| 122 | + #if DEBUG |
| 123 | + if let isInvalidated { |
| 124 | + let debugger = BindableActionDebugger<Self>( |
| 125 | + value: value, |
| 126 | + isInvalidated: isInvalidated |
| 127 | + ) |
| 128 | + return Self.binding( |
| 129 | + .init( |
| 130 | + keyPath: keyPath, |
| 131 | + set: { |
| 132 | + debugger.wasCalled.setValue(true) |
| 133 | + $0[keyPath: keyPath] = value |
| 134 | + }, |
| 135 | + value: value, |
| 136 | + valueIsEqualTo: { $0 as? Value == value } |
| 137 | + ) |
| 138 | + ) |
| 139 | + } |
| 140 | + #endif |
| 141 | + return Self.binding( |
| 142 | + .init( |
| 143 | + keyPath: keyPath, |
| 144 | + set: { $0[keyPath: keyPath] = value }, |
| 145 | + value: value, |
| 146 | + valueIsEqualTo: { $0 as? Value == value } |
| 147 | + ) |
| 148 | + ) |
| 149 | + } |
| 150 | + |
82 | 151 | public static func set<Value: Equatable & Sendable>(
|
83 | 152 | _ keyPath: _WritableKeyPath<State, Value>,
|
84 | 153 | _ value: Value
|
85 | 154 | ) -> Self {
|
86 |
| - self.binding(.set(keyPath, value)) |
| 155 | + self.set(keyPath, value, isInvalidated: nil) |
87 | 156 | }
|
88 | 157 | }
|
89 | 158 |
|
|
94 | 163 | get { self.state[keyPath: keyPath] }
|
95 | 164 | set {
|
96 | 165 | BindingLocal.$isActive.withValue(true) {
|
97 |
| - self.send(.binding(.set(keyPath, newValue))) |
| 166 | + self.send(.set(keyPath, newValue, isInvalidated: _isInvalidated)) |
98 | 167 | }
|
99 | 168 | }
|
100 | 169 | }
|
|
111 | 180 | get { self.observableState }
|
112 | 181 | set {
|
113 | 182 | BindingLocal.$isActive.withValue(true) {
|
114 |
| - self.send(.binding(.set(\.self, newValue))) |
| 183 | + self.send(.set(\.self, newValue, isInvalidated: _isInvalidated)) |
115 | 184 | }
|
116 | 185 | }
|
117 | 186 | }
|
|
130 | 199 | get { self.state[keyPath: keyPath] }
|
131 | 200 | set {
|
132 | 201 | BindingLocal.$isActive.withValue(true) {
|
133 |
| - self.send(.view(.binding(.set(keyPath, newValue)))) |
| 202 | + self.send(.view(.set(keyPath, newValue, isInvalidated: _isInvalidated))) |
134 | 203 | }
|
135 | 204 | }
|
136 | 205 | }
|
|
148 | 217 | get { self.observableState }
|
149 | 218 | set {
|
150 | 219 | BindingLocal.$isActive.withValue(true) {
|
151 |
| - self.send(.view(.binding(.set(\.self, newValue)))) |
| 220 | + self.send(.view(.set(\.self, newValue, isInvalidated: _isInvalidated))) |
152 | 221 | }
|
153 | 222 | }
|
154 | 223 | }
|
|
0 commit comments