Skip to content

Commit c4134c9

Browse files
mluisbrownmbrandonwaroben
committed
Safer, Conciser Bindings (#765)
* Better binding tools. * make everything public * deprecate * update * clean up * wip * dml * wip * wip * Fix a redundant conformance constraint warning in ForEachStore (#738) The warning was: Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift:141:28: warning: redundant conformance constraint 'EachContent' : 'View' public init<EachContent: View>( ^ Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift:146:16: note: conformance constraint 'EachContent' : 'View' implied here EachContent: View, ^ I fixed the warning by removing the redundant constraint from the type parameter. * wip * wip * wip * wip * wip * wip * wip Co-authored-by: Brandon Williams <[email protected]> Co-authored-by: Adam Roben <[email protected]>
1 parent 8ddcade commit c4134c9

File tree

6 files changed

+392
-192
lines changed

6 files changed

+392
-192
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,31 @@ on:
1010

1111
jobs:
1212
library:
13-
runs-on: macos-10.15
14-
strategy:
15-
matrix:
16-
xcode:
17-
- 12.4
18-
steps:
19-
- uses: actions/checkout@v2
20-
- name: Select Xcode ${{ matrix.xcode }}
21-
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
22-
- name: Run tests
23-
run: make test-library
24-
- name: Run benchmark
25-
if: ${{ matrix.xcode != 11.3 }}
26-
run: make benchmark
27-
28-
library-beta:
2913
runs-on: macos-11.0
3014
strategy:
3115
matrix:
3216
xcode:
17+
- 12.5
3318
- '13.0'
3419
steps:
3520
- uses: actions/checkout@v2
3621
- name: Select Xcode ${{ matrix.xcode }}
3722
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
38-
- name: Compile documentation
39-
run: make test-docs
4023
- name: Run tests
4124
run: make test-library
25+
- name: Compile documentation
26+
if: ${{ matrix.xcode == '13.0' }}
27+
run: make test-docs
4228
- name: Run benchmark
4329
run: make benchmark
4430

4531
examples:
46-
runs-on: macos-10.15
32+
runs-on: macos-11.0
4733
strategy:
4834
matrix:
4935
xcode:
50-
- 12.4
36+
- 12.5
37+
- '13.0'
5138
steps:
5239
- uses: actions/checkout@v2
5340
- name: Select Xcode ${{ matrix.xcode }}

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

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,25 @@ import SwiftUI
33

44
private let readMe = """
55
This file demonstrates how to handle two-way bindings in the Composable Architecture using \
6-
binding actions.
6+
bindable state and actions.
77
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.
8+
Bindable state and actions allow you to safely eliminate the boilerplate caused by needing to \
9+
have a unique action for every UI control. Instead, all UI bindings can be consolidated into a \
10+
single `binding` action that holds onto a `BindingAction` value, and all bindable state can be \
11+
safeguarded with the `BindableState` property wrapper.
1112
1213
It is instructive to compare this case study to the "Binding Basics" case study.
1314
"""
1415

1516
// The state for this screen holds a bunch of values that will drive
1617
struct BindingFormState: Equatable {
17-
var sliderValue = 5.0
18-
var stepCount = 10
19-
var text = ""
20-
var toggleIsOn = false
18+
@BindableState var sliderValue = 5.0
19+
@BindableState var stepCount = 10
20+
@BindableState var text = ""
21+
@BindableState var toggleIsOn = false
2122
}
2223

23-
enum BindingFormAction: Equatable {
24+
enum BindingFormAction: BindableAction, Equatable {
2425
case binding(BindingAction<BindingFormState>)
2526
case resetButtonTapped
2627
}
@@ -32,7 +33,7 @@ let bindingFormReducer = Reducer<
3233
> {
3334
state, action, _ in
3435
switch action {
35-
case .binding(\.stepCount):
36+
case .binding(\.$stepCount):
3637
state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount))
3738
return .none
3839

@@ -44,7 +45,7 @@ let bindingFormReducer = Reducer<
4445
return .none
4546
}
4647
}
47-
.binding(action: /BindingFormAction.binding)
48+
.binding()
4849

4950
struct BindingFormView: View {
5051
let store: Store<BindingFormState, BindingFormAction>
@@ -54,24 +55,17 @@ struct BindingFormView: View {
5455
Form {
5556
Section(header: Text(template: readMe, .caption)) {
5657
HStack {
57-
TextField(
58-
"Type here",
59-
text: viewStore.binding(keyPath: \.text, send: BindingFormAction.binding)
60-
)
61-
.disableAutocorrection(true)
62-
.foregroundColor(viewStore.toggleIsOn ? .gray : .primary)
58+
TextField("Type here", text: viewStore.$text)
59+
.disableAutocorrection(true)
60+
.foregroundColor(viewStore.toggleIsOn ? .gray : .primary)
61+
6362
Text(alternate(viewStore.text))
6463
}
6564
.disabled(viewStore.toggleIsOn)
6665

67-
Toggle(isOn: viewStore.binding(keyPath: \.toggleIsOn, send: BindingFormAction.binding)) {
68-
Text("Disable other controls")
69-
}
66+
Toggle("Disable other controls", isOn: viewStore.$toggleIsOn)
7067

71-
Stepper(
72-
value: viewStore.binding(keyPath: \.stepCount, send: BindingFormAction.binding),
73-
in: 0...100
74-
) {
68+
Stepper(value: viewStore.$stepCount, in: 0...100) {
7569
Text("Max slider value: \(viewStore.stepCount)")
7670
.font(Font.body.monospacedDigit())
7771
}
@@ -80,10 +74,8 @@ struct BindingFormView: View {
8074
HStack {
8175
Text("Slider value: \(Int(viewStore.sliderValue))")
8276
.font(Font.body.monospacedDigit())
83-
Slider(
84-
value: viewStore.binding(keyPath: \.sliderValue, send: BindingFormAction.binding),
85-
in: 0...Double(viewStore.stepCount)
86-
)
77+
78+
Slider(value: viewStore.$sliderValue, in: 0...Double(viewStore.stepCount))
8779
}
8880
.disabled(viewStore.toggleIsOn)
8981
}

Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-FocusState.swift

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ private let readMe = """
77
"""
88

99
struct FocusDemoState: Equatable {
10-
var focusedField: Field? = nil
11-
var password: String = ""
12-
var username: String = ""
10+
@BindableState var focusedField: Field? = nil
11+
@BindableState var password: String = ""
12+
@BindableState var username: String = ""
1313

1414
enum Field: String, Hashable {
1515
case username, password
1616
}
1717
}
1818

19-
enum FocusDemoAction: Equatable {
19+
enum FocusDemoAction: BindableAction, Equatable {
2020
case binding(BindingAction<FocusDemoState>)
2121
case signInButtonTapped
2222
}
@@ -41,70 +41,61 @@ let focusDemoReducer = Reducer<
4141
return .none
4242
}
4343
}
44-
.binding(action: /FocusDemoAction.binding)
44+
.binding()
4545

4646
#if compiler(>=5.5)
47-
struct FocusDemoView: View {
48-
let store: Store<FocusDemoState, FocusDemoAction>
49-
@FocusState var focusedField: FocusDemoState.Field?
47+
struct FocusDemoView: View {
48+
let store: Store<FocusDemoState, FocusDemoAction>
49+
@FocusState var focusedField: FocusDemoState.Field?
5050

51-
var body: some View {
52-
WithViewStore(self.store) { viewStore in
53-
VStack(alignment: .leading, spacing: 32) {
54-
Text(template: readMe, .caption)
51+
var body: some View {
52+
WithViewStore(self.store) { viewStore in
53+
VStack(alignment: .leading, spacing: 32) {
54+
Text(template: readMe, .caption)
5555

56-
VStack {
57-
TextField(
58-
"Username",
59-
text: viewStore.binding(keyPath: \.username, send: FocusDemoAction.binding)
60-
)
56+
VStack {
57+
TextField("Username", text: viewStore.$username)
6158
.focused($focusedField, equals: .username)
6259

63-
SecureField(
64-
"Password",
65-
text: viewStore.binding(keyPath: \.password, send: FocusDemoAction.binding)
66-
)
60+
SecureField("Password", text: viewStore.$password)
6761
.focused($focusedField, equals: .password)
6862

69-
Button("Sign In") {
70-
viewStore.send(.signInButtonTapped)
63+
Button("Sign In") {
64+
viewStore.send(.signInButtonTapped)
65+
}
7166
}
72-
}
7367

74-
Spacer()
68+
Spacer()
69+
}
70+
.padding()
71+
.synchronize(viewStore.$focusedField, self.$focusedField)
7572
}
76-
.padding()
77-
.synchronize(
78-
viewStore.binding(keyPath: \.focusedField, send: FocusDemoAction.binding),
79-
self.$focusedField
80-
)
73+
.navigationBarTitle("Focus demo")
8174
}
82-
.navigationBarTitle("Focus demo")
8375
}
84-
}
8576

86-
extension View {
87-
func synchronize<Value: Equatable>(
88-
_ first: Binding<Value>,
89-
_ second: FocusState<Value>.Binding
90-
) -> some View {
91-
self
92-
.onChange(of: first.wrappedValue) { second.wrappedValue = $0 }
93-
.onChange(of: second.wrappedValue) { first.wrappedValue = $0 }
77+
extension View {
78+
func synchronize<Value: Equatable>(
79+
_ first: Binding<Value>,
80+
_ second: FocusState<Value>.Binding
81+
) -> some View {
82+
self
83+
.onChange(of: first.wrappedValue) { second.wrappedValue = $0 }
84+
.onChange(of: second.wrappedValue) { first.wrappedValue = $0 }
85+
}
9486
}
95-
}
9687

97-
struct FocusDemo_Previews: PreviewProvider {
98-
static var previews: some View {
99-
NavigationView {
100-
FocusDemoView(
101-
store: Store(
102-
initialState: .init(),
103-
reducer: focusDemoReducer,
104-
environment: .init()
88+
struct FocusDemo_Previews: PreviewProvider {
89+
static var previews: some View {
90+
NavigationView {
91+
FocusDemoView(
92+
store: Store(
93+
initialState: .init(),
94+
reducer: focusDemoReducer,
95+
environment: .init()
96+
)
10597
)
106-
)
98+
}
10799
}
108100
}
109-
}
110101
#endif

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

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

15-
store.send(.binding(.set(\.sliderValue, 2))) {
15+
store.send(.set(\.$sliderValue, 2)) {
1616
$0.sliderValue = 2
1717
}
18-
store.send(.binding(.set(\.stepCount, 1))) {
18+
store.send(.set(\.$stepCount, 1)) {
1919
$0.sliderValue = 1
2020
$0.stepCount = 1
2121
}
22-
store.send(.binding(.set(\.text, "Blob"))) {
22+
store.send(.set(\.$text, "Blob")) {
2323
$0.text = "Blob"
2424
}
25-
store.send(.binding(.set(\.toggleIsOn, true))) {
25+
store.send(.set(\.$toggleIsOn, true)) {
2626
$0.toggleIsOn = true
2727
}
2828
store.send(.resetButtonTapped) {

0 commit comments

Comments
 (0)