Skip to content

Commit 6220382

Browse files
mluisbrownmbrandonw
andcommitted
Forms (#367)
* Forms * wip * wip * Basics * Fix * Apply .textCase(.none) to section headeres. * Small tweaks do docs and case study readme. * Update Forms.swift * Fix warnings * Revert "Apply .textCase(.none) to section headeres." This reverts commit f535a75eb9f6ebcc49c13411c760e8e630a21902. * fix Co-authored-by: Brandon Williams <[email protected]>
1 parent 9cd2649 commit 6220382

File tree

33 files changed

+542
-27
lines changed

33 files changed

+542
-27
lines changed

.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1200"
3+
LastUpgradeVersion = "1240"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture_watchOS.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1200"
3+
LastUpgradeVersion = "1240"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
DC25DC5F2450F13200082E81 /* IfLetStoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */; };
4545
DC25DC612450F2B000082E81 /* LoadThenNavigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC602450F2B000082E81 /* LoadThenNavigate.swift */; };
4646
DC25DC642450F2DF00082E81 /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC632450F2DF00082E81 /* ActivityIndicatorViewController.swift */; };
47+
DC27215625BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */; };
4748
DC2E370D24573ACB00B94699 /* 04-HigherOrderReducers-StrictReducers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2E370C24573ACB00B94699 /* 04-HigherOrderReducers-StrictReducers.swift */; };
4849
DC4C6EAC2450DD380066A05D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6EAB2450DD380066A05D /* SceneDelegate.swift */; };
4950
DC4C6EAE2450DD380066A05D /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6EAD2450DD380066A05D /* RootViewController.swift */; };
@@ -54,6 +55,7 @@
5455
DC4C6ED62450E1050066A05D /* CounterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6ED52450E1050066A05D /* CounterViewController.swift */; };
5556
DC4C6ED82450E4570066A05D /* UIViewRepresented.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6ED72450E4570066A05D /* UIViewRepresented.swift */; };
5657
DC4C6EDA2450E6050066A05D /* NavigateAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6ED92450E6050066A05D /* NavigateAndLoad.swift */; };
58+
DC5B505125C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */; };
5759
DC630FDA2451016B00BAECBA /* ListsOfState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC630FD92451016B00BAECBA /* ListsOfState.swift */; };
5860
DC634B252448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */; };
5961
DC88D8A6245341EC0077F427 /* 01-GettingStarted-Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */; };
@@ -182,6 +184,7 @@
182184
DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfLetStoreController.swift; sourceTree = "<group>"; };
183185
DC25DC602450F2B000082E81 /* LoadThenNavigate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadThenNavigate.swift; sourceTree = "<group>"; };
184186
DC25DC632450F2DF00082E81 /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = "<group>"; };
187+
DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-BindingBasicsTests.swift"; sourceTree = "<group>"; };
185188
DC2E370C24573ACB00B94699 /* 04-HigherOrderReducers-StrictReducers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-StrictReducers.swift"; sourceTree = "<group>"; };
186189
DC4C6EA72450DD380066A05D /* UIKitCaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIKitCaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; };
187190
DC4C6EAB2450DD380066A05D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@@ -196,6 +199,7 @@
196199
DC4C6ED52450E1050066A05D /* CounterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterViewController.swift; sourceTree = "<group>"; };
197200
DC4C6ED72450E4570066A05D /* UIViewRepresented.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewRepresented.swift; sourceTree = "<group>"; };
198201
DC4C6ED92450E6050066A05D /* NavigateAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateAndLoad.swift; sourceTree = "<group>"; };
202+
DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Forms.swift"; sourceTree = "<group>"; };
199203
DC630FD92451016B00BAECBA /* ListsOfState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsOfState.swift; sourceTree = "<group>"; };
200204
DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableFavoritingTests.swift"; sourceTree = "<group>"; };
201205
DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Animations.swift"; sourceTree = "<group>"; };
@@ -387,6 +391,7 @@
387391
CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndActionSheets.swift */,
388392
DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */,
389393
CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */,
394+
DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */,
390395
DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */,
391396
DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */,
392397
DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */,
@@ -421,6 +426,7 @@
421426
children = (
422427
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */,
423428
CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */,
429+
DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */,
424430
4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */,
425431
CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */,
426432
CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */,
@@ -584,7 +590,7 @@
584590
isa = PBXProject;
585591
attributes = {
586592
LastSwiftUpdateCheck = 1150;
587-
LastUpgradeCheck = 1200;
593+
LastUpgradeCheck = 1240;
588594
ORGANIZATIONNAME = "Point-Free";
589595
TargetAttributes = {
590596
CAF88E6F24B8E26D00539345 = {
@@ -749,6 +755,7 @@
749755
CA6AC2642451135C00C71CB3 /* ReusableComponents-Download.swift in Sources */,
750756
CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */,
751757
CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */,
758+
DC5B505125C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift in Sources */,
752759
CAA9ADC22446587C0003A984 /* 02-Effects-Basics.swift in Sources */,
753760
DC89C41B24460F95006900B9 /* 00-RootView.swift in Sources */,
754761
DCC68EDD2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift in Sources */,
@@ -772,6 +779,7 @@
772779
isa = PBXSourcesBuildPhase;
773780
buildActionMask = 2147483647;
774781
files = (
782+
DC27215625BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift in Sources */,
775783
CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift in Sources */,
776784
DC634B252448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift in Sources */,
777785
CAA9ADC824465D950003A984 /* 02-Effects-CancellationTests.swift in Sources */,
@@ -1006,6 +1014,7 @@
10061014
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
10071015
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
10081016
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
1017+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
10091018
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
10101019
CLANG_WARN_STRICT_PROTOTYPES = YES;
10111020
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -1065,6 +1074,7 @@
10651074
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
10661075
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
10671076
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
1077+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
10681078
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
10691079
CLANG_WARN_STRICT_PROTOTYPES = YES;
10701080
CLANG_WARN_SUSPICIOUS_MOVE = YES;

Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (SwiftUI).xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1200"
3+
LastUpgradeVersion = "1240"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (UIKit).xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1200"
3+
LastUpgradeVersion = "1240"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct RootState {
66
var alertAndActionSheet = AlertAndSheetState()
77
var animation = AnimationsState()
88
var bindingBasics = BindingBasicsState()
9+
var bindingForm = BindingFormState()
910
var clock = ClockState()
1011
var counter = CounterState()
1112
var dieRoll = DieRollState()
@@ -35,6 +36,7 @@ enum RootAction {
3536
case alertAndActionSheet(AlertAndSheetAction)
3637
case animation(AnimationsAction)
3738
case bindingBasics(BindingBasicsAction)
39+
case bindingForm(BindingFormAction)
3840
case clock(ClockAction)
3941
case counter(CounterAction)
4042
case dieRoll(DieRollAction)

Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ struct RootView: View {
4141
)
4242
)
4343

44+
NavigationLink(
45+
"Form bindings",
46+
destination: BindingFormView(
47+
store: self.store.scope(
48+
state: { $0.bindingForm },
49+
action: RootAction.bindingForm
50+
)
51+
)
52+
)
53+
4454
NavigationLink(
4555
"Optional state",
4656
destination: OptionalBasicsView(
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import ComposableArchitecture
2+
import SwiftUI
3+
4+
private let readMe = """
5+
This file demonstrates how to handle two-way bindings in the Composable Architecture using form \
6+
actions.
7+
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.
11+
12+
It is instructive to compare this case study to the "Binding Basics" case study.
13+
"""
14+
15+
// The state for this screen holds a bunch of values that will drive
16+
struct BindingFormState: Equatable {
17+
var sliderValue = 5.0
18+
var stepCount = 10
19+
var text = ""
20+
var toggleIsOn = false
21+
}
22+
23+
enum BindingFormAction: Equatable {
24+
case form(FormAction<BindingFormState>)
25+
case resetButtonTapped
26+
}
27+
28+
struct BindingFormEnvironment {}
29+
30+
let bindingFormReducer = Reducer<
31+
BindingFormState, BindingFormAction, BindingFormEnvironment
32+
> {
33+
state, action, _ in
34+
switch action {
35+
case .form(\.stepCount):
36+
state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount))
37+
return .none
38+
39+
case .form:
40+
return .none
41+
42+
case .resetButtonTapped:
43+
state = .init()
44+
return .none
45+
}
46+
}
47+
.form(action: /BindingFormAction.form)
48+
49+
struct BindingFormView: View {
50+
let store: Store<BindingFormState, BindingFormAction>
51+
52+
var body: some View {
53+
WithViewStore(self.store) { viewStore in
54+
Form {
55+
Section(header: Text(template: readMe, .caption)) {
56+
HStack {
57+
TextField(
58+
"Type here",
59+
text: viewStore.binding(keyPath: \.text, send: BindingFormAction.form)
60+
)
61+
.disableAutocorrection(true)
62+
.foregroundColor(viewStore.toggleIsOn ? .gray : .primary)
63+
Text(alternate(viewStore.text))
64+
}
65+
.disabled(viewStore.toggleIsOn)
66+
67+
Toggle(isOn: viewStore.binding(keyPath: \.toggleIsOn, send: BindingFormAction.form)) {
68+
Text("Disable other controls")
69+
}
70+
71+
Stepper(
72+
value: viewStore.binding(keyPath: \.stepCount, send: BindingFormAction.form),
73+
in: 0...100
74+
) {
75+
Text("Max slider value: \(viewStore.stepCount)")
76+
.font(Font.body.monospacedDigit())
77+
}
78+
.disabled(viewStore.toggleIsOn)
79+
80+
HStack {
81+
Text("Slider value: \(Int(viewStore.sliderValue))")
82+
.font(Font.body.monospacedDigit())
83+
Slider(
84+
value: viewStore.binding(keyPath: \.sliderValue, send: BindingFormAction.form),
85+
in: 0...Double(viewStore.stepCount)
86+
)
87+
}
88+
.disabled(viewStore.toggleIsOn)
89+
}
90+
}
91+
}
92+
.navigationBarTitle("Bindings form")
93+
}
94+
}
95+
96+
private func alternate(_ string: String) -> String {
97+
string
98+
.enumerated()
99+
.map { idx, char in
100+
idx.isMultiple(of: 2)
101+
? char.uppercased()
102+
: char.lowercased()
103+
}
104+
.joined()
105+
}
106+
107+
struct BindingFormView_Previews: PreviewProvider {
108+
static var previews: some View {
109+
NavigationView {
110+
BindingFormView(
111+
store: Store(
112+
initialState: BindingFormState(),
113+
reducer: bindingFormReducer,
114+
environment: BindingFormEnvironment()
115+
)
116+
)
117+
}
118+
}
119+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Combine
2+
import ComposableArchitecture
3+
import XCTest
4+
5+
@testable import SwiftUICaseStudies
6+
7+
class BindingFormTests: XCTestCase {
8+
func testBasics() {
9+
let store = TestStore(
10+
initialState: BindingFormState(),
11+
reducer: bindingFormReducer,
12+
environment: BindingFormEnvironment()
13+
)
14+
15+
store.assert(
16+
.send(.form(.set(\.sliderValue, 2))) {
17+
$0.sliderValue = 2
18+
},
19+
.send(.form(.set(\.stepCount, 1))) {
20+
$0.sliderValue = 1
21+
$0.stepCount = 1
22+
},
23+
.send(.form(.set(\.text, "Blob"))) {
24+
$0.text = "Blob"
25+
},
26+
.send(.form(.set(\.toggleIsOn, true))) {
27+
$0.toggleIsOn = true
28+
},
29+
.send(.resetButtonTapped) {
30+
$0 = .init(sliderValue: 5, stepCount: 10, text: "", toggleIsOn: false)
31+
}
32+
)
33+
}
34+
}

Examples/Search/Search.xcodeproj/project.pbxproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@
181181
isa = PBXProject;
182182
attributes = {
183183
LastSwiftUpdateCheck = 1140;
184-
LastUpgradeCheck = 1140;
184+
LastUpgradeCheck = 1240;
185185
ORGANIZATIONNAME = "Brandon Williams";
186186
TargetAttributes = {
187187
CA86E49624253C2500357AD9 = {
@@ -289,6 +289,7 @@
289289
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
290290
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
291291
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
292+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
292293
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
293294
CLANG_WARN_STRICT_PROTOTYPES = YES;
294295
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -348,6 +349,7 @@
348349
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
349350
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
350351
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
352+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
351353
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
352354
CLANG_WARN_STRICT_PROTOTYPES = YES;
353355
CLANG_WARN_SUSPICIOUS_MOVE = YES;

0 commit comments

Comments
 (0)