Skip to content

Commit 95d2bc7

Browse files
authored
Better Bindings Revisited (#2215)
* Simpler bindable view state * wip * wip * wip * wip * wip * wip * simplify * wip * wip * wip * wip * wip * wip * wip
1 parent b9defab commit 95d2bc7

File tree

27 files changed

+534
-639
lines changed

27 files changed

+534
-639
lines changed

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,30 +60,26 @@ struct BindingFormView: View {
6060
}
6161

6262
HStack {
63-
TextField("Type here", text: viewStore.binding(\.$text))
63+
TextField("Type here", text: viewStore.$text)
6464
.disableAutocorrection(true)
6565
.foregroundStyle(viewStore.toggleIsOn ? Color.secondary : .primary)
6666
Text(alternate(viewStore.text))
6767
}
6868
.disabled(viewStore.toggleIsOn)
6969

70-
Toggle(
71-
"Disable other controls",
72-
isOn: viewStore.binding(\.$toggleIsOn)
73-
.resignFirstResponder()
74-
)
70+
Toggle("Disable other controls", isOn: viewStore.$toggleIsOn.resignFirstResponder())
7571

7672
Stepper(
7773
"Max slider value: \(viewStore.stepCount)",
78-
value: viewStore.binding(\.$stepCount),
74+
value: viewStore.$stepCount,
7975
in: 0...100
8076
)
8177
.disabled(viewStore.toggleIsOn)
8278

8379
HStack {
8480
Text("Slider value: \(Int(viewStore.sliderValue))")
8581

86-
Slider(value: viewStore.binding(\.$sliderValue), in: 0...Double(viewStore.stepCount))
82+
Slider(value: viewStore.$sliderValue, in: 0...Double(viewStore.stepCount))
8783
.tint(.accentColor)
8884
}
8985
.disabled(viewStore.toggleIsOn)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ struct FocusDemoView: View {
5555
AboutView(readMe: readMe)
5656

5757
VStack {
58-
TextField("Username", text: viewStore.binding(\.$username))
58+
TextField("Username", text: viewStore.$username)
5959
.focused($focusedField, equals: .username)
60-
SecureField("Password", text: viewStore.binding(\.$password))
60+
SecureField("Password", text: viewStore.$password)
6161
.focused($focusedField, equals: .password)
6262
Button("Sign In") {
6363
viewStore.send(.signInButtonTapped)
@@ -66,7 +66,7 @@ struct FocusDemoView: View {
6666
}
6767
.textFieldStyle(.roundedBorder)
6868
}
69-
.synchronize(viewStore.binding(\.$focusedField), self.$focusedField)
69+
.synchronize(viewStore.$focusedField, self.$focusedField)
7070
}
7171
.navigationTitle("Focus demo")
7272
}

Examples/Integration/Integration/BindingLocalTestCase.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ private struct ChildView: View {
135135
Button("Dismiss") {
136136
self.dismiss()
137137
}
138-
TextField("Text", text: viewStore.binding(\.$text))
138+
TextField("Text", text: viewStore.$text)
139139
Button(viewStore.sendOnDisappear ? "Don't send onDisappear" : "Send onDisappear") {
140-
viewStore.binding(\.$sendOnDisappear).wrappedValue.toggle()
140+
viewStore.$sendOnDisappear.wrappedValue.toggle()
141141
}
142142
}
143143
.onDisappear {

Examples/Integration/Integration/PresentationTestCase.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ private struct ChildView: View {
417417
WithViewStore(self.store, observe: { $0 }) { viewStore in
418418
VStack {
419419
Text("Count: \(viewStore.count)")
420-
TextField("Text field", text: viewStore.binding(\.$text))
420+
TextField("Text field", text: viewStore.$text)
421421
Button("Child dismiss") {
422422
viewStore.send(.childDismissButtonTapped)
423423
}

Examples/Standups/Standups/StandupForm.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,21 @@ struct StandupFormView: View {
6565
WithViewStore(self.store, observe: { $0 }) { viewStore in
6666
Form {
6767
Section {
68-
TextField("Title", text: viewStore.binding(\.$standup.title))
68+
TextField("Title", text: viewStore.$standup.title)
6969
.focused(self.$focus, equals: .title)
7070
HStack {
71-
Slider(value: viewStore.binding(\.$standup.duration).seconds, in: 5...30, step: 1) {
71+
Slider(value: viewStore.$standup.duration.seconds, in: 5...30, step: 1) {
7272
Text("Length")
7373
}
7474
Spacer()
7575
Text(viewStore.standup.duration.formatted(.units()))
7676
}
77-
ThemePicker(selection: viewStore.binding(\.$standup.theme))
77+
ThemePicker(selection: viewStore.$standup.theme)
7878
} header: {
7979
Text("Standup Info")
8080
}
8181
Section {
82-
ForEach(viewStore.binding(\.$standup.attendees)) { $attendee in
82+
ForEach(viewStore.$standup.attendees) { $attendee in
8383
TextField("Name", text: $attendee.name)
8484
.focused(self.$focus, equals: .attendee(attendee.id))
8585
}
@@ -94,7 +94,7 @@ struct StandupFormView: View {
9494
Text("Attendees")
9595
}
9696
}
97-
.bind(viewStore.binding(\.$focus), to: self.$focus)
97+
.bind(viewStore.$focus, to: self.$focus)
9898
}
9999
}
100100
}

Examples/TicTacToe/tic-tac-toe/Package.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ let package = Package(
8686
name: "GameSwiftUI",
8787
dependencies: ["GameCore"]
8888
),
89-
.testTarget(
90-
name: "GameSwiftUITests",
91-
dependencies: ["GameSwiftUI"]
92-
),
9389
.target(
9490
name: "GameUIKit",
9591
dependencies: ["GameCore"]
@@ -114,10 +110,6 @@ let package = Package(
114110
"TwoFactorSwiftUI",
115111
]
116112
),
117-
.testTarget(
118-
name: "LoginSwiftUITests",
119-
dependencies: ["LoginSwiftUI"]
120-
),
121113
.target(
122114
name: "LoginUIKit",
123115
dependencies: [
@@ -144,10 +136,6 @@ let package = Package(
144136
"NewGameCore",
145137
]
146138
),
147-
.testTarget(
148-
name: "NewGameSwiftUITests",
149-
dependencies: ["NewGameSwiftUI"]
150-
),
151139
.target(
152140
name: "NewGameUIKit",
153141
dependencies: [
@@ -171,10 +159,6 @@ let package = Package(
171159
name: "TwoFactorSwiftUI",
172160
dependencies: ["TwoFactorCore"]
173161
),
174-
.testTarget(
175-
name: "TwoFactorSwiftUITests",
176-
dependencies: ["TwoFactorSwiftUI"]
177-
),
178162
.target(
179163
name: "TwoFactorUIKit",
180164
dependencies: ["TwoFactorCore"]

Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,25 @@ import TwoFactorCore
66
public struct Login: ReducerProtocol, Sendable {
77
public struct State: Equatable {
88
@PresentationState public var alert: AlertState<AlertAction>?
9-
public var email = ""
9+
@BindingState public var email = ""
1010
public var isFormValid = false
1111
public var isLoginRequestInFlight = false
12-
public var password = ""
12+
@BindingState public var password = ""
1313
@PresentationState public var twoFactor: TwoFactor.State?
1414

1515
public init() {}
1616
}
1717

18-
public enum Action: Equatable, Sendable {
18+
public enum Action: Equatable {
1919
case alert(PresentationAction<AlertAction>)
20-
case emailChanged(String)
21-
case passwordChanged(String)
22-
case loginButtonTapped
2320
case loginResponse(TaskResult<AuthenticationResponse>)
2421
case twoFactor(PresentationAction<TwoFactor.Action>)
22+
case view(View)
23+
24+
public enum View: BindableAction, Equatable {
25+
case binding(BindingAction<State>)
26+
case loginButtonTapped
27+
}
2528
}
2629

2730
public enum AlertAction: Equatable, Sendable {}
@@ -31,16 +34,12 @@ public struct Login: ReducerProtocol, Sendable {
3134
public init() {}
3235

3336
public var body: some ReducerProtocol<State, Action> {
37+
BindingReducer(action: /Action.view)
3438
Reduce { state, action in
3539
switch action {
3640
case .alert:
3741
return .none
3842

39-
case let .emailChanged(email):
40-
state.email = email
41-
state.isFormValid = !state.email.isEmpty && !state.password.isEmpty
42-
return .none
43-
4443
case let .loginResponse(.success(response)):
4544
state.isLoginRequestInFlight = false
4645
if response.twoFactorRequired {
@@ -53,12 +52,14 @@ public struct Login: ReducerProtocol, Sendable {
5352
state.isLoginRequestInFlight = false
5453
return .none
5554

56-
case let .passwordChanged(password):
57-
state.password = password
55+
case .twoFactor:
56+
return .none
57+
58+
case .view(.binding):
5859
state.isFormValid = !state.email.isEmpty && !state.password.isEmpty
5960
return .none
6061

61-
case .loginButtonTapped:
62+
case .view(.loginButtonTapped):
6263
state.isLoginRequestInFlight = true
6364
return .run { [email = state.email, password = state.password] send in
6465
await send(
@@ -71,9 +72,6 @@ public struct Login: ReducerProtocol, Sendable {
7172
)
7273
)
7374
}
74-
75-
case .twoFactor:
76-
return .none
7775
}
7876
}
7977
.ifLet(\.$alert, action: /Action.alert)

Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift

Lines changed: 17 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,19 @@ public struct LoginView: View {
99
let store: StoreOf<Login>
1010

1111
struct ViewState: Equatable {
12-
var alert: AlertState<Login.AlertAction>?
13-
var email: String
12+
@BindingViewState var email: String
1413
var isActivityIndicatorVisible: Bool
1514
var isFormDisabled: Bool
1615
var isLoginButtonDisabled: Bool
17-
var password: String
18-
19-
init(state: Login.State) {
20-
self.alert = state.alert
21-
self.email = state.email
22-
self.isActivityIndicatorVisible = state.isLoginRequestInFlight
23-
self.isFormDisabled = state.isLoginRequestInFlight
24-
self.isLoginButtonDisabled = !state.isFormValid
25-
self.password = state.password
26-
}
27-
}
28-
29-
enum ViewAction {
30-
case emailChanged(String)
31-
case loginButtonTapped
32-
case passwordChanged(String)
16+
@BindingViewState var password: String
3317
}
3418

3519
public init(store: StoreOf<Login>) {
3620
self.store = store
3721
}
3822

3923
public var body: some View {
40-
WithViewStore(self.store, observe: ViewState.init, send: Login.Action.init) { viewStore in
24+
WithViewStore(self.store, observe: \.view, send: { .view($0) }) { viewStore in
4125
Form {
4226
Text(
4327
"""
@@ -48,18 +32,12 @@ public struct LoginView: View {
4832
)
4933

5034
Section {
51-
TextField(
52-
53-
text: viewStore.binding(get: \.email, send: ViewAction.emailChanged)
54-
)
55-
.autocapitalization(.none)
56-
.keyboardType(.emailAddress)
57-
.textContentType(.emailAddress)
35+
TextField("[email protected]", text: viewStore.$email)
36+
.autocapitalization(.none)
37+
.keyboardType(.emailAddress)
38+
.textContentType(.emailAddress)
5839

59-
SecureField(
60-
"••••••••",
61-
text: viewStore.binding(get: \.password, send: ViewAction.passwordChanged)
62-
)
40+
SecureField("••••••••", text: viewStore.$password)
6341
}
6442

6543
Button {
@@ -93,16 +71,15 @@ public struct LoginView: View {
9371
}
9472
}
9573

96-
extension Login.Action {
97-
init(action: LoginView.ViewAction) {
98-
switch action {
99-
case let .emailChanged(email):
100-
self = .emailChanged(email)
101-
case .loginButtonTapped:
102-
self = .loginButtonTapped
103-
case let .passwordChanged(password):
104-
self = .passwordChanged(password)
105-
}
74+
extension BindingViewStore<Login.State> {
75+
var view: LoginView.ViewState {
76+
LoginView.ViewState(
77+
email: self.$email,
78+
isActivityIndicatorVisible: self.isLoginRequestInFlight,
79+
isFormDisabled: self.isLoginRequestInFlight,
80+
isLoginButtonDisabled: !self.isFormValid,
81+
password: self.$password
82+
)
10683
}
10784
}
10885

Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,11 @@ extension Login.Action {
200200
case .alertDismissed:
201201
self = .alert(.dismiss)
202202
case let .emailChanged(email):
203-
self = .emailChanged(email ?? "")
203+
self = .view(.set(\.$email, email ?? ""))
204204
case .loginButtonTapped:
205-
self = .loginButtonTapped
205+
self = .view(.loginButtonTapped)
206206
case let .passwordChanged(password):
207-
self = .passwordChanged(password ?? "")
207+
self = .view(.set(\.$password, password ?? ""))
208208
case .twoFactorDismissed:
209209
self = .twoFactor(.dismiss)
210210
}

0 commit comments

Comments
 (0)