-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathBinding.swift
242 lines (234 loc) · 8.44 KB
/
Binding.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
import CasePaths
import SwiftUI
/// An action that describes simple mutations to some root state at a writable key path.
///
/// This type can be used to eliminate the boilerplate that is typically incurred when working with
/// multiple mutable fields on state.
///
/// For example, a settings screen may model its state with the following struct:
///
/// struct SettingsState {
/// var digest = Digest.daily
/// var displayName = ""
/// var enableNotifications = false
/// var protectMyPosts = false
/// var sendEmailNotifications = false
/// var sendMobileNotifications = false
/// }
///
/// Each of these fields should be editable, and in the Composable Architecture this means that each
/// field requires a corresponding action that can be sent to the store. Typically this comes in the
/// form of an enum with a case per field:
///
/// enum SettingsAction {
/// case digestChanged(Digest)
/// case displayNameChanged(String)
/// case enableNotificationsChanged(Bool)
/// case protectMyPostsChanged(Bool)
/// case sendEmailNotificationsChanged(Bool)
/// case sendMobileNotificationsChanged(Bool)
/// }
///
/// And we're not even done yet. In the reducer we must now handle each action, which simply
/// replaces the state at each field with a new value:
///
/// let settingsReducer = Reducer<
/// SettingsState, SettingsAction, SettingsEnvironment
/// > { state, action, environment in
/// switch action {
/// case let digestChanged(digest):
/// state.digest = digest
/// return .none
///
/// case let displayNameChanged(displayName):
/// state.displayName = displayName
/// return .none
///
/// case let enableNotificationsChanged(isOn):
/// state.enableNotifications = isOn
/// return .none
///
/// case let protectMyPostsChanged(isOn):
/// state.protectMyPosts = isOn
/// return .none
///
/// case let sendEmailNotificationsChanged(isOn):
/// state.sendEmailNotifications = isOn
/// return .none
///
/// case let sendMobileNotificationsChanged(isOn):
/// state.sendMobileNotifications = isOn
/// return .none
/// }
/// }
///
/// This is a _lot_ of boilerplate for something that should be simple. Luckily, we can dramatically
/// eliminate this boilerplate using `BindingAction`. First, we can collapse all of these
/// field-mutating actions into a single case that holds a `BindingAction` generic over the
/// reducer's root `SettingsState`:
///
/// enum SettingsAction {
/// case binding(BindingAction<SettingsState>)
/// }
///
/// And then, we can simplify the settings reducer by allowing the `binding` method to handle these
/// field mutations for us:
///
/// let settingsReducer = Reducer<
/// SettingsState, SettingsAction, SettingsEnvironment
/// > {
/// switch action {
/// case .binding:
/// return .none
/// }
/// }
/// .binding(action: /SettingsAction.binding)
///
/// Binding actions are constructed and sent to the store by providing a writable key path from root
/// state to the field being mutated. There is even a view store helper that simplifies this work.
/// You can derive a binding by specifying the key path and binding action case:
///
/// TextField(
/// "Display name",
/// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.binding)
/// )
///
/// Should you need to layer additional functionality over these bindings, your reducer can pattern
/// match the action for a given key path:
///
/// case .binding(\.displayName):
/// // Validate display name
///
/// case .binding(\.enableNotifications):
/// // Return an authorization request effect
///
/// Binding actions can also be tested in much the same way regular actions are tested. Rather than
/// send a specific action describing how a binding changed, such as `displayNameChanged("Blob")`,
/// you will send a `.binding` action that describes which key path is being set to what value, such
/// as `.binding(.set(\.displayName, "Blob"))`:
///
/// let store = TestStore(
/// initialState: SettingsState(),
/// reducer: settingsReducer,
/// environment: SettingsEnvironment(...)
/// )
///
/// store.assert(
/// .send(.binding(.set(\.displayName, "Blob"))) {
/// $0.displayName = "Blob"
/// },
/// .send(.binding(.set(\.protectMyPosts, true))) {
/// $0.protectMyPosts = true
/// )
/// )
///
public struct BindingAction<Root>: Equatable {
public let keyPath: PartialKeyPath<Root>
fileprivate let set: (inout Root) -> Void
private let value: Any
private let valueIsEqualTo: (Any) -> Bool
/// Returns an action that describes simple mutations to some root state at a writable key path.
///
/// - Parameters:
/// - keyPath: A key path to the property that should be mutated.
/// - value: A value to assign at the given key path.
/// - Returns: An action that describes simple mutations to some root state at a writable key
/// path.
public static func set<Value>(
_ keyPath: WritableKeyPath<Root, Value>,
_ value: Value
) -> Self
where Value: Equatable {
.init(
keyPath: keyPath,
set: { $0[keyPath: keyPath] = value },
value: value,
valueIsEqualTo: { $0 as? Value == value }
)
}
/// Transforms a binding action over some root state to some other type of root state given a key
/// path.
///
/// - Parameter keyPath: A key path from a new type of root state to the original root state.
/// - Returns: A binding action over a new type of root state.
public func pullback<NewRoot>(
_ keyPath: WritableKeyPath<NewRoot, Root>
) -> BindingAction<NewRoot> {
.init(
keyPath: (keyPath as AnyKeyPath).appending(path: self.keyPath) as! PartialKeyPath<NewRoot>,
set: { self.set(&$0[keyPath: keyPath]) },
value: self.value,
valueIsEqualTo: self.valueIsEqualTo
)
}
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.keyPath == rhs.keyPath && lhs.valueIsEqualTo(rhs.value)
}
public static func ~= <Value>(
keyPath: WritableKeyPath<Root, Value>,
bindingAction: Self
) -> Bool {
keyPath == bindingAction.keyPath
}
}
extension Reducer {
/// Returns a reducer that applies `BindingAction` mutations to `State` before running this
/// reducer's logic.
///
/// For example, a settings screen may gather its binding actions into a single `BindingAction`
/// case:
///
/// enum SettingsAction {
/// ...
/// case binding(BindingAction<SettingsState>)
/// }
///
/// The reducer can then be enhanced to automatically handle these mutations for you by tacking on
/// the `binding` method:
///
/// let settingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment {
/// ...
/// }
/// .binding(action: /SettingsAction.binding)
///
/// - Parameter toBindingAction: A case path from this reducer's `Action` type to a
/// `BindingAction` over this reducer's `State`.
/// - Returns: A reducer that applies `BindingAction` mutations to `State` before running this
/// reducer's logic.
public func binding(action toBindingAction: CasePath<Action, BindingAction<State>>) -> Self {
Self { state, action, environment in
toBindingAction.extract(from: action)?.set(&state)
return .none
}
.combined(with: self)
}
}
extension ViewStore {
/// Derives a binding from the store that mutates state at the given writable key path by wrapping
/// a `BindingAction` with the store's action type.
///
/// For example, a text field binding can be created like this:
///
/// struct State { var text = "" }
/// enum Action { case binding(BindingAction<State>) }
///
/// TextField(
/// "Enter text",
/// text: viewStore.binding(keyPath: \.text, Action.binding)
/// )
///
/// - Parameters:
/// - keyPath: A writable key path from the view store's state to a mutable field
/// - action: A function that wraps a binding action in the view store's action type.
/// - Returns: A binding.
public func binding<LocalState>(
keyPath: WritableKeyPath<State, LocalState>,
send action: @escaping (BindingAction<State>) -> Action
) -> Binding<LocalState>
where LocalState: Equatable {
self.binding(
get: { $0[keyPath: keyPath] },
send: { action(.set(keyPath, $0)) }
)
}
}