Skip to content

Commit 5bdf0bc

Browse files
authored
FormAction → BindingAction (#372)
* Rename FormAction to BindingAction * Deprecations * Fix test * Update 01-GettingStarted-Bindings-Forms.swift
1 parent e51fb00 commit 5bdf0bc

File tree

4 files changed

+73
-58
lines changed

4 files changed

+73
-58
lines changed

Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import ComposableArchitecture
22
import SwiftUI
33

44
private let readMe = """
5-
This file demonstrates how to handle two-way bindings in the Composable Architecture using form \
6-
actions.
5+
This file demonstrates how to handle two-way bindings in the Composable Architecture using \
6+
binding actions.
77
8-
Form actions allow you to eliminate the boilerplate caused by needing to have a unique action \
9-
for every UI control. Instead, all UI bindings can be consolidated into a single `form` action \
10-
that holds onto a `FormAction` value.
8+
Binding actions allow you to eliminate the boilerplate caused by needing to have a unique action \
9+
for every UI control. Instead, all UI bindings can be consolidated into a single `binding` \
10+
action that holds onto a `BindingAction` value.
1111
1212
It is instructive to compare this case study to the "Binding Basics" case study.
1313
"""
@@ -21,7 +21,7 @@ struct BindingFormState: Equatable {
2121
}
2222

2323
enum BindingFormAction: Equatable {
24-
case form(FormAction<BindingFormState>)
24+
case binding(BindingAction<BindingFormState>)
2525
case resetButtonTapped
2626
}
2727

@@ -32,19 +32,19 @@ let bindingFormReducer = Reducer<
3232
> {
3333
state, action, _ in
3434
switch action {
35-
case .form(\.stepCount):
35+
case .binding(\.stepCount):
3636
state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount))
3737
return .none
3838

39-
case .form:
39+
case .binding:
4040
return .none
4141

4242
case .resetButtonTapped:
4343
state = .init()
4444
return .none
4545
}
4646
}
47-
.form(action: /BindingFormAction.form)
47+
.binding(action: /BindingFormAction.binding)
4848

4949
struct BindingFormView: View {
5050
let store: Store<BindingFormState, BindingFormAction>
@@ -56,20 +56,20 @@ struct BindingFormView: View {
5656
HStack {
5757
TextField(
5858
"Type here",
59-
text: viewStore.binding(keyPath: \.text, send: BindingFormAction.form)
59+
text: viewStore.binding(keyPath: \.text, send: BindingFormAction.binding)
6060
)
6161
.disableAutocorrection(true)
6262
.foregroundColor(viewStore.toggleIsOn ? .gray : .primary)
6363
Text(alternate(viewStore.text))
6464
}
6565
.disabled(viewStore.toggleIsOn)
6666

67-
Toggle(isOn: viewStore.binding(keyPath: \.toggleIsOn, send: BindingFormAction.form)) {
67+
Toggle(isOn: viewStore.binding(keyPath: \.toggleIsOn, send: BindingFormAction.binding)) {
6868
Text("Disable other controls")
6969
}
7070

7171
Stepper(
72-
value: viewStore.binding(keyPath: \.stepCount, send: BindingFormAction.form),
72+
value: viewStore.binding(keyPath: \.stepCount, send: BindingFormAction.binding),
7373
in: 0...100
7474
) {
7575
Text("Max slider value: \(viewStore.stepCount)")
@@ -81,7 +81,7 @@ struct BindingFormView: View {
8181
Text("Slider value: \(Int(viewStore.sliderValue))")
8282
.font(Font.body.monospacedDigit())
8383
Slider(
84-
value: viewStore.binding(keyPath: \.sliderValue, send: BindingFormAction.form),
84+
value: viewStore.binding(keyPath: \.sliderValue, send: BindingFormAction.binding),
8585
in: 0...Double(viewStore.stepCount)
8686
)
8787
}

Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ class BindingFormTests: XCTestCase {
1313
)
1414

1515
store.assert(
16-
.send(.form(.set(\.sliderValue, 2))) {
16+
.send(.binding(.set(\.sliderValue, 2))) {
1717
$0.sliderValue = 2
1818
},
19-
.send(.form(.set(\.stepCount, 1))) {
19+
.send(.binding(.set(\.stepCount, 1))) {
2020
$0.sliderValue = 1
2121
$0.stepCount = 1
2222
},
23-
.send(.form(.set(\.text, "Blob"))) {
23+
.send(.binding(.set(\.text, "Blob"))) {
2424
$0.text = "Blob"
2525
},
26-
.send(.form(.set(\.toggleIsOn, true))) {
26+
.send(.binding(.set(\.toggleIsOn, true))) {
2727
$0.toggleIsOn = true
2828
},
2929
.send(.resetButtonTapped) {

Sources/ComposableArchitecture/Internal/Deprecations.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import Combine
22
import SwiftUI
33

4+
// NB: Deprecated after 0.13.0:
5+
6+
@available(*, deprecated, renamed: "BindingAction")
7+
public typealias FormAction = BindingAction
8+
9+
extension Reducer {
10+
@available(*, deprecated, renamed: "binding")
11+
public func form(action toFormAction: CasePath<Action, BindingAction<State>>) -> Self {
12+
self.binding(action: toFormAction)
13+
}
14+
}
15+
416
// NB: Deprecated after 0.10.0:
517

618
@available(iOS 13, *)

Sources/ComposableArchitecture/SwiftUI/Forms.swift renamed to Sources/ComposableArchitecture/SwiftUI/Binding.swift

Lines changed: 44 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -64,49 +64,49 @@ import SwiftUI
6464
/// }
6565
///
6666
/// This is a _lot_ of boilerplate for something that should be simple. Luckily, we can dramatically
67-
/// eliminate this boilerplate using `FormAction`. First, we can collapse all of these
68-
/// field-mutating actions into a single case that holds a `FormAction` generic over the reducer's
69-
/// root `SettingsState`:
67+
/// eliminate this boilerplate using `BindingAction`. First, we can collapse all of these
68+
/// field-mutating actions into a single case that holds a `BindingAction` generic over the
69+
/// reducer's root `SettingsState`:
7070
///
7171
/// enum SettingsAction {
72-
/// case form(FormAction<SettingsState>)
72+
/// case binding(BindingAction<SettingsState>)
7373
/// }
7474
///
75-
/// And then, we can simplify the settings reducer by allowing the `form` method to handle these
75+
/// And then, we can simplify the settings reducer by allowing the `binding` method to handle these
7676
/// field mutations for us:
7777
///
7878
/// let settingsReducer = Reducer<
7979
/// SettingsState, SettingsAction, SettingsEnvironment
8080
/// > {
8181
/// switch action {
82-
/// case .form:
82+
/// case .binding:
8383
/// return .none
8484
/// }
8585
/// }
86-
/// .form(action: /SettingsAction.form)
86+
/// .binding(action: /SettingsAction.binding)
8787
///
88-
/// Form actions are constructed and sent to the store by providing a writable key path from root
88+
/// Binding actions are constructed and sent to the store by providing a writable key path from root
8989
/// state to the field being mutated. There is even a view store helper that simplifies this work.
90-
/// You can derive a binding by specifying the key path and form action case:
90+
/// You can derive a binding by specifying the key path and binding action case:
9191
///
9292
/// TextField(
9393
/// "Display name",
94-
/// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.form)
94+
/// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.binding)
9595
/// )
9696
///
97-
/// Should you need to layer additional functionality over your form, your reducer can pattern match
98-
/// the form action for a given key path:
97+
/// Should you need to layer additional functionality over these bindings, your reducer can pattern
98+
/// match the action for a given key path:
9999
///
100-
/// case .form(\.displayName):
100+
/// case .binding(\.displayName):
101101
/// // Validate display name
102102
///
103-
/// case .form(\.enableNotifications):
103+
/// case .binding(\.enableNotifications):
104104
/// // Return an authorization request effect
105105
///
106-
/// Form actions can also be tested in much the same way regular actions are tested. Rather than
106+
/// Binding actions can also be tested in much the same way regular actions are tested. Rather than
107107
/// send a specific action describing how a binding changed, such as `displayNameChanged("Blob")`,
108-
/// you will send a `.form` action that describes which key path is being set to what value, such
109-
/// as `.form(.set(\.displayName, "Blob"))`:
108+
/// you will send a `.binding` action that describes which key path is being set to what value, such
109+
/// as `.binding(.set(\.displayName, "Blob"))`:
110110
///
111111
/// let store = TestStore(
112112
/// initialState: SettingsState(),
@@ -115,15 +115,15 @@ import SwiftUI
115115
/// )
116116
///
117117
/// store.assert(
118-
/// .send(.form(.set(\.displayName, "Blob"))) {
118+
/// .send(.binding(.set(\.displayName, "Blob"))) {
119119
/// $0.displayName = "Blob"
120120
/// },
121-
/// .send(.form(.set(\.protectMyPosts, true))) {
121+
/// .send(.binding(.set(\.protectMyPosts, true))) {
122122
/// $0.protectMyPosts = true
123123
/// )
124124
/// )
125125
///
126-
public struct FormAction<Root>: Equatable {
126+
public struct BindingAction<Root>: Equatable {
127127
public let keyPath: PartialKeyPath<Root>
128128

129129
fileprivate let set: (inout Root) -> Void
@@ -150,12 +150,14 @@ public struct FormAction<Root>: Equatable {
150150
)
151151
}
152152

153-
/// Transforms a form action over some root state to some other type of root state given a key
153+
/// Transforms a binding action over some root state to some other type of root state given a key
154154
/// path.
155155
///
156156
/// - Parameter keyPath: A key path from a new type of root state to the original root state.
157-
/// - Returns: A form action over a new type of root state.
158-
public func pullback<NewRoot>(_ keyPath: WritableKeyPath<NewRoot, Root>) -> FormAction<NewRoot> {
157+
/// - Returns: A binding action over a new type of root state.
158+
public func pullback<NewRoot>(
159+
_ keyPath: WritableKeyPath<NewRoot, Root>
160+
) -> BindingAction<NewRoot> {
159161
.init(
160162
keyPath: (keyPath as AnyKeyPath).appending(path: self.keyPath) as! PartialKeyPath<NewRoot>,
161163
set: { self.set(&$0[keyPath: keyPath]) },
@@ -170,38 +172,39 @@ public struct FormAction<Root>: Equatable {
170172

171173
public static func ~= <Value>(
172174
keyPath: WritableKeyPath<Root, Value>,
173-
formAction: FormAction<Root>
175+
bindingAction: Self
174176
) -> Bool {
175-
keyPath == formAction.keyPath
177+
keyPath == bindingAction.keyPath
176178
}
177179
}
178180

179181
extension Reducer {
180-
/// Returns a reducer that applies `FormAction` mutations to `State` before running this reducer's
181-
/// logic.
182+
/// Returns a reducer that applies `BindingAction` mutations to `State` before running this
183+
/// reducer's logic.
182184
///
183-
/// For example, a settings screen may gather its form actions into a single `FormAction` case:
185+
/// For example, a settings screen may gather its binding actions into a single `BindingAction`
186+
/// case:
184187
///
185188
/// enum SettingsAction {
186189
/// ...
187-
/// case form(FormAction<SettingsState>)
190+
/// case binding(BindingAction<SettingsState>)
188191
/// }
189192
///
190193
/// The reducer can then be enhanced to automatically handle these mutations for you by tacking on
191-
/// the `form` method:
194+
/// the `binding` method:
192195
///
193196
/// let settingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment {
194197
/// ...
195198
/// }
196-
/// .form(action: /SettingsAction.form)
199+
/// .binding(action: /SettingsAction.binding)
197200
///
198-
/// - Parameter toFormAction: A case path from this reducer's `Action` type to a `FormAction` over
199-
/// this reducer's `State`.
200-
/// - Returns: A reducer that applies `FormAction` mutations to `State` before running this
201+
/// - Parameter toBindingAction: A case path from this reducer's `Action` type to a
202+
/// `BindingAction` over this reducer's `State`.
203+
/// - Returns: A reducer that applies `BindingAction` mutations to `State` before running this
201204
/// reducer's logic.
202-
public func form(action toFormAction: CasePath<Action, FormAction<State>>) -> Self {
205+
public func binding(action toBindingAction: CasePath<Action, BindingAction<State>>) -> Self {
203206
Self { state, action, environment in
204-
toFormAction.extract(from: action)?.set(&state)
207+
toBindingAction.extract(from: action)?.set(&state)
205208
return .none
206209
}
207210
.combined(with: self)
@@ -210,25 +213,25 @@ extension Reducer {
210213

211214
extension ViewStore {
212215
/// Derives a binding from the store that mutates state at the given writable key path by wrapping
213-
/// a `FormAction` with the store's action type.
216+
/// a `BindingAction` with the store's action type.
214217
///
215218
/// For example, a text field binding can be created like this:
216219
///
217220
/// struct State { var text = "" }
218-
/// enum Action { case form(FormAction<State>) }
221+
/// enum Action { case binding(BindingAction<State>) }
219222
///
220223
/// TextField(
221224
/// "Enter text",
222-
/// text: viewStore.binding(keyPath: \.text, Action.form)
225+
/// text: viewStore.binding(keyPath: \.text, Action.binding)
223226
/// )
224227
///
225228
/// - Parameters:
226229
/// - keyPath: A writable key path from the view store's state to a mutable field
227-
/// - action: A function that wraps a form action in the view store's action type.
230+
/// - action: A function that wraps a binding action in the view store's action type.
228231
/// - Returns: A binding.
229232
public func binding<LocalState>(
230233
keyPath: WritableKeyPath<State, LocalState>,
231-
send action: @escaping (FormAction<State>) -> Action
234+
send action: @escaping (BindingAction<State>) -> Action
232235
) -> Binding<LocalState>
233236
where LocalState: Equatable {
234237
self.binding(

0 commit comments

Comments
 (0)