Skip to content

Commit 30015d1

Browse files
authored
Revert #1790 (#1795)
It occurred to us that this solution unfortunately is incompatible with view actions. We have an alternate solution that works, so I'll PR that in the future if no others materialize!
1 parent d1c2e5b commit 30015d1

File tree

14 files changed

+224
-654
lines changed

14 files changed

+224
-654
lines changed

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ private let readMe = """
88
Bindable state and actions allow you to safely eliminate the boilerplate caused by needing to \
99
have a unique action for every UI control. Instead, all UI bindings can be consolidated into a \
1010
single `binding` action that holds onto a `BindingAction` value, and all bindable state can be \
11-
safeguarded with the `BindingState` property wrapper.
11+
safeguarded with the `BindableState` property wrapper.
1212
1313
It is instructive to compare this case study to the "Binding Basics" case study.
1414
"""
1515

1616
// MARK: - Feature domain
1717

1818
struct BindingForm: ReducerProtocol {
19-
struct State: BindableStateProtocol, Equatable {
20-
@BindingState var sliderValue = 5.0
21-
@BindingState var stepCount = 10
22-
@BindingState var text = ""
23-
@BindingState var toggleIsOn = false
19+
struct State: Equatable {
20+
@BindableState var sliderValue = 5.0
21+
@BindableState var stepCount = 10
22+
@BindableState var text = ""
23+
@BindableState var toggleIsOn = false
2424
}
2525

2626
enum Action: BindableAction, Equatable {
@@ -60,7 +60,7 @@ struct BindingFormView: View {
6060
}
6161

6262
HStack {
63-
TextField("Type here", text: viewStore.$text)
63+
TextField("Type here", text: viewStore.binding(\.$text))
6464
.disableAutocorrection(true)
6565
.foregroundStyle(viewStore.toggleIsOn ? Color.secondary : .primary)
6666
Text(alternate(viewStore.text))
@@ -69,18 +69,21 @@ struct BindingFormView: View {
6969

7070
Toggle(
7171
"Disable other controls",
72-
isOn: viewStore.$toggleIsOn.resignFirstResponder()
72+
isOn: viewStore.binding(\.$toggleIsOn)
73+
.resignFirstResponder()
7374
)
7475

7576
Stepper(
76-
"Max slider value: \(viewStore.stepCount)", value: viewStore.$stepCount, in: 0...100
77+
"Max slider value: \(viewStore.stepCount)",
78+
value: viewStore.binding(\.$stepCount),
79+
in: 0...100
7780
)
7881
.disabled(viewStore.toggleIsOn)
7982

8083
HStack {
8184
Text("Slider value: \(Int(viewStore.sliderValue))")
8285

83-
Slider(value: viewStore.$sliderValue, in: 0...Double(viewStore.stepCount))
86+
Slider(value: viewStore.binding(\.$sliderValue), in: 0...Double(viewStore.stepCount))
8487
.tint(.accentColor)
8588
}
8689
.disabled(viewStore.toggleIsOn)

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ private let readMe = """
99
// MARK: - Feature domain
1010

1111
struct FocusDemo: ReducerProtocol {
12-
struct State: BindableStateProtocol, Equatable {
13-
@BindingState var focusedField: Field?
14-
@BindingState var password: String = ""
15-
@BindingState var username: String = ""
12+
struct State: Equatable {
13+
@BindableState var focusedField: Field?
14+
@BindableState var password: String = ""
15+
@BindableState var username: String = ""
1616

1717
enum Field: String, Hashable {
1818
case username, password
@@ -55,11 +55,9 @@ struct FocusDemoView: View {
5555
AboutView(readMe: readMe)
5656

5757
VStack {
58-
59-
TextField("Username", text: viewStore.$username)
58+
TextField("Username", text: viewStore.binding(\.$username))
6059
.focused($focusedField, equals: .username)
61-
62-
SecureField("Password", text: viewStore.$password)
60+
SecureField("Password", text: viewStore.binding(\.$password))
6361
.focused($focusedField, equals: .password)
6462
Button("Sign In") {
6563
viewStore.send(.signInButtonTapped)
@@ -68,7 +66,7 @@ struct FocusDemoView: View {
6866
}
6967
.textFieldStyle(.roundedBorder)
7068
}
71-
.synchronize(viewStore.$focusedField, self.$focusedField)
69+
.synchronize(viewStore.binding(\.$focusedField), self.$focusedField)
7270
}
7371
.navigationTitle("Focus demo")
7472
}

Examples/Todos/Todos/Todo.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@ import ComposableArchitecture
22
import SwiftUI
33

44
struct Todo: ReducerProtocol {
5-
struct State: BindableStateProtocol, Equatable, Identifiable {
6-
@BindingState var description = ""
5+
struct State: Equatable, Identifiable {
6+
var description = ""
77
let id: UUID
8-
@BindingState var isComplete = false
8+
var isComplete = false
99
}
1010

11-
enum Action: BindableAction, Equatable {
12-
case binding(BindingAction<State>)
11+
enum Action: Equatable {
12+
case checkBoxToggled
13+
case textFieldChanged(String)
1314
}
1415

15-
var body: some ReducerProtocol<State, Action> {
16-
BindingReducer()
16+
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
17+
switch action {
18+
case .checkBoxToggled:
19+
state.isComplete.toggle()
20+
return .none
21+
22+
case let .textFieldChanged(description):
23+
state.description = description
24+
return .none
25+
}
1726
}
1827
}
1928

@@ -23,12 +32,15 @@ struct TodoView: View {
2332
var body: some View {
2433
WithViewStore(self.store, observe: { $0 }) { viewStore in
2534
HStack {
26-
Button(action: { viewStore.$isComplete.wrappedValue.toggle() }) {
35+
Button(action: { viewStore.send(.checkBoxToggled) }) {
2736
Image(systemName: viewStore.isComplete ? "checkmark.square" : "square")
2837
}
2938
.buttonStyle(.plain)
3039

31-
TextField("Untitled Todo", text: viewStore.$description)
40+
TextField(
41+
"Untitled Todo",
42+
text: viewStore.binding(get: \.description, send: Todo.Action.textFieldChanged)
43+
)
3244
}
3345
.foregroundColor(viewStore.isComplete ? .gray : nil)
3446
}

Examples/Todos/Todos/Todos.swift

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ enum Filter: LocalizedStringKey, CaseIterable, Hashable {
88
}
99

1010
struct Todos: ReducerProtocol {
11-
struct State: BindableStateProtocol, Equatable {
12-
@BindingState var editMode: EditMode = .inactive
13-
@BindingState var filter: Filter = .all
11+
struct State: Equatable {
12+
var editMode: EditMode = .inactive
13+
var filter: Filter = .all
1414
var todos: IdentifiedArrayOf<Todo.State> = []
1515

1616
var filteredTodos: IdentifiedArrayOf<Todo.State> {
@@ -22,11 +22,12 @@ struct Todos: ReducerProtocol {
2222
}
2323
}
2424

25-
enum Action: BindableAction, Equatable, Sendable {
25+
enum Action: Equatable {
2626
case addTodoButtonTapped
27-
case binding(BindingAction<State>)
2827
case clearCompletedButtonTapped
2928
case delete(IndexSet)
29+
case editModeChanged(EditMode)
30+
case filterPicked(Filter)
3031
case move(IndexSet, Int)
3132
case sortCompletedTodos
3233
case todo(id: Todo.State.ID, action: Todo.Action)
@@ -37,16 +38,12 @@ struct Todos: ReducerProtocol {
3738
private enum TodoCompletionID {}
3839

3940
var body: some ReducerProtocol<State, Action> {
40-
BindingReducer()
4141
Reduce { state, action in
4242
switch action {
4343
case .addTodoButtonTapped:
4444
state.todos.insert(Todo.State(id: self.uuid()), at: 0)
4545
return .none
4646

47-
case .binding:
48-
return .none
49-
5047
case .clearCompletedButtonTapped:
5148
state.todos.removeAll(where: \.isComplete)
5249
return .none
@@ -58,6 +55,14 @@ struct Todos: ReducerProtocol {
5855
}
5956
return .none
6057

58+
case let .editModeChanged(editMode):
59+
state.editMode = editMode
60+
return .none
61+
62+
case let .filterPicked(filter):
63+
state.filter = filter
64+
return .none
65+
6166
case var .move(source, destination):
6267
if state.filter == .completed {
6368
source = IndexSet(
@@ -83,7 +88,7 @@ struct Todos: ReducerProtocol {
8388
state.todos.sort { $1.isComplete && !$0.isComplete }
8489
return .none
8590

86-
case .todo(id: _, action: .binding(\.$isComplete)):
91+
case .todo(id: _, action: .checkBoxToggled):
8792
return .run { send in
8893
try await self.clock.sleep(for: .seconds(1))
8994
await send(.sortCompletedTodos, animation: .default)
@@ -102,56 +107,70 @@ struct Todos: ReducerProtocol {
102107

103108
struct AppView: View {
104109
let store: StoreOf<Todos>
110+
@ObservedObject var viewStore: ViewStore<ViewState, Todos.Action>
111+
112+
init(store: StoreOf<Todos>) {
113+
self.store = store
114+
self.viewStore = ViewStore(self.store.scope(state: ViewState.init(state:)))
115+
}
105116

106117
struct ViewState: Equatable {
107-
@BindingViewState var editMode: EditMode
108-
@BindingViewState var filter: Filter
118+
let editMode: EditMode
119+
let filter: Filter
109120
let isClearCompletedButtonDisabled: Bool
110121

111122
init(state: Todos.State) {
112-
self._editMode = state.bindings.$editMode
113-
self._filter = state.bindings.$filter
123+
self.editMode = state.editMode
124+
self.filter = state.filter
114125
self.isClearCompletedButtonDisabled = !state.todos.contains(where: \.isComplete)
115126
}
116127
}
117128

118129
var body: some View {
119-
WithViewStore(self.store, observe: ViewState.init) { viewStore in
120-
NavigationView {
121-
VStack(alignment: .leading) {
122-
Picker("Filter", selection: viewStore.$filter.animation()) {
123-
ForEach(Filter.allCases, id: \.self) { filter in
124-
Text(filter.rawValue).tag(filter)
125-
}
126-
}
127-
.pickerStyle(.segmented)
128-
.padding(.horizontal)
129-
130-
List {
131-
ForEachStore(
132-
self.store.scope(state: \.filteredTodos, action: Todos.Action.todo(id:action:))
133-
) {
134-
TodoView(store: $0)
135-
}
136-
.onDelete { viewStore.send(.delete($0)) }
137-
.onMove { viewStore.send(.move($0, $1)) }
130+
NavigationView {
131+
VStack(alignment: .leading) {
132+
Picker(
133+
"Filter",
134+
selection: self.viewStore.binding(
135+
get: \.filter,
136+
send: Todos.Action.filterPicked
137+
)
138+
.animation()
139+
) {
140+
ForEach(Filter.allCases, id: \.self) { filter in
141+
Text(filter.rawValue).tag(filter)
138142
}
139143
}
140-
.navigationTitle("Todos")
141-
.navigationBarItems(
142-
trailing: HStack(spacing: 20) {
143-
EditButton()
144-
Button("Clear Completed") {
145-
viewStore.send(.clearCompletedButtonTapped, animation: .default)
146-
}
147-
.disabled(viewStore.isClearCompletedButtonDisabled)
148-
Button("Add Todo") { viewStore.send(.addTodoButtonTapped, animation: .default) }
144+
.pickerStyle(.segmented)
145+
.padding(.horizontal)
146+
147+
List {
148+
ForEachStore(
149+
self.store.scope(state: \.filteredTodos, action: Todos.Action.todo(id:action:))
150+
) {
151+
TodoView(store: $0)
149152
}
150-
)
151-
.environment(\.editMode, viewStore.$editMode)
153+
.onDelete { self.viewStore.send(.delete($0)) }
154+
.onMove { self.viewStore.send(.move($0, $1)) }
155+
}
152156
}
153-
.navigationViewStyle(.stack)
157+
.navigationTitle("Todos")
158+
.navigationBarItems(
159+
trailing: HStack(spacing: 20) {
160+
EditButton()
161+
Button("Clear Completed") {
162+
self.viewStore.send(.clearCompletedButtonTapped, animation: .default)
163+
}
164+
.disabled(self.viewStore.isClearCompletedButtonDisabled)
165+
Button("Add Todo") { self.viewStore.send(.addTodoButtonTapped, animation: .default) }
166+
}
167+
)
168+
.environment(
169+
\.editMode,
170+
self.viewStore.binding(get: \.editMode, send: Todos.Action.editModeChanged)
171+
)
154172
}
173+
.navigationViewStyle(.stack)
155174
}
156175
}
157176

Examples/Todos/TodosTests/TodosTests.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ final class TodosTests: XCTestCase {
5959
)
6060

6161
await store.send(
62-
.todo(id: state.todos[0].id, action: .set(\.$description, "Learn Composable Architecture"))
62+
.todo(id: state.todos[0].id, action: .textFieldChanged("Learn Composable Architecture"))
6363
) {
6464
$0.todos[id: state.todos[0].id]?.description = "Learn Composable Architecture"
6565
}
@@ -88,7 +88,7 @@ final class TodosTests: XCTestCase {
8888

8989
store.dependencies.continuousClock = self.clock
9090

91-
await store.send(.todo(id: state.todos[0].id, action: .set(\.$isComplete, true))) {
91+
await store.send(.todo(id: state.todos[0].id, action: .checkBoxToggled)) {
9292
$0.todos[id: state.todos[0].id]?.isComplete = true
9393
}
9494
await self.clock.advance(by: .seconds(1))
@@ -123,11 +123,11 @@ final class TodosTests: XCTestCase {
123123

124124
store.dependencies.continuousClock = self.clock
125125

126-
await store.send(.todo(id: state.todos[0].id, action: .set(\.$isComplete, true))) {
126+
await store.send(.todo(id: state.todos[0].id, action: .checkBoxToggled)) {
127127
$0.todos[id: state.todos[0].id]?.isComplete = true
128128
}
129129
await self.clock.advance(by: .milliseconds(500))
130-
await store.send(.todo(id: state.todos[0].id, action: .set(\.$isComplete, false))) {
130+
await store.send(.todo(id: state.todos[0].id, action: .checkBoxToggled)) {
131131
$0.todos[id: state.todos[0].id]?.isComplete = false
132132
}
133133
await self.clock.advance(by: .seconds(1))
@@ -259,7 +259,7 @@ final class TodosTests: XCTestCase {
259259

260260
store.dependencies.continuousClock = self.clock
261261

262-
await store.send(.set(\.$editMode, .active)) {
262+
await store.send(.editModeChanged(.active)) {
263263
$0.editMode = .active
264264
}
265265
await store.send(.move([0], 2)) {
@@ -307,10 +307,10 @@ final class TodosTests: XCTestCase {
307307
store.dependencies.continuousClock = self.clock
308308
store.dependencies.uuid = .incrementing
309309

310-
await store.send(.set(\.$editMode, .active)) {
310+
await store.send(.editModeChanged(.active)) {
311311
$0.editMode = .active
312312
}
313-
await store.send(.set(\.$filter, .completed)) {
313+
await store.send(.filterPicked(.completed)) {
314314
$0.filter = .completed
315315
}
316316
await store.send(.move([0], 2)) {
@@ -346,11 +346,10 @@ final class TodosTests: XCTestCase {
346346
reducer: Todos()
347347
)
348348

349-
await store.send(.set(\.$filter, .completed)) {
349+
await store.send(.filterPicked(.completed)) {
350350
$0.filter = .completed
351351
}
352-
await store.send(.todo(id: state.todos[1].id, action: .set(\.$description, "Did this already")))
353-
{
352+
await store.send(.todo(id: state.todos[1].id, action: .textFieldChanged("Did this already"))) {
354353
$0.todos[id: state.todos[1].id]?.description = "Did this already"
355354
}
356355
}

0 commit comments

Comments
 (0)