Skip to content

Commit 31c7270

Browse files
mbrandonwmluisbrown
authored andcommitted
Clean up the recursive case study. (#1403)
(cherry picked from commit 99e493b1784a91eba09903a2f7c5c8ea2f6f8c78)
1 parent b272014 commit 31c7270

File tree

3 files changed

+115
-56
lines changed

3 files changed

+115
-56
lines changed

Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */; };
2424
CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */; };
2525
CA6AC2672451135C00C71CB3 /* DownloadClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2632451135C00C71CB3 /* DownloadClient.swift */; };
26+
CA78F0CD28DA47D70026C4AD /* 04-HigherOrderReducers-RecursionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA78F0CC28DA47D70026C4AD /* 04-HigherOrderReducers-RecursionTests.swift */; };
2627
CA7BC8EE245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */; };
2728
CAA9ADC22446587C0003A984 /* 02-Effects-Basics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */; };
2829
CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */; };
@@ -167,6 +168,7 @@
167168
CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
168169
CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadComponent.swift; sourceTree = "<group>"; };
169170
CA6AC2632451135C00C71CB3 /* DownloadClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadClient.swift; sourceTree = "<group>"; };
171+
CA78F0CC28DA47D70026C4AD /* 04-HigherOrderReducers-RecursionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-RecursionTests.swift"; sourceTree = "<group>"; };
170172
CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-SharedState.swift"; sourceTree = "<group>"; };
171173
CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Basics.swift"; sourceTree = "<group>"; };
172174
CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-BasicsTests.swift"; sourceTree = "<group>"; };
@@ -445,6 +447,7 @@
445447
DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */,
446448
CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */,
447449
CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */,
450+
CA78F0CC28DA47D70026C4AD /* 04-HigherOrderReducers-RecursionTests.swift */,
448451
DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */,
449452
CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */,
450453
);
@@ -798,6 +801,7 @@
798801
DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */,
799802
CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */,
800803
CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */,
804+
CA78F0CD28DA47D70026C4AD /* 04-HigherOrderReducers-RecursionTests.swift in Sources */,
801805
4F5AC11F24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift in Sources */,
802806
CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */,
803807
);

Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Recursion.swift

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ extension Reducer {
2828
}
2929

3030
struct NestedState: Equatable, Identifiable {
31-
var children: IdentifiedArrayOf<NestedState> = []
3231
let id: UUID
33-
var description: String = ""
32+
var name: String = ""
33+
var rows: IdentifiedArrayOf<NestedState> = []
3434
}
3535

36-
indirect enum NestedAction: Equatable {
37-
case append
38-
case node(id: NestedState.ID, action: NestedAction)
39-
case remove(IndexSet)
40-
case rename(String)
36+
enum NestedAction: Equatable {
37+
case addRowButtonTapped
38+
case nameTextFieldChanged(String)
39+
case onDelete(IndexSet)
40+
indirect case row(id: NestedState.ID, action: NestedAction)
4141
}
4242

4343
struct NestedEnvironment {
@@ -48,49 +48,49 @@ let nestedReducer = Reducer<
4848
NestedState, NestedAction, NestedEnvironment
4949
>.recurse { `self`, state, action, environment in
5050
switch action {
51-
case .append:
52-
state.children.append(NestedState(id: environment.uuid()))
51+
case .addRowButtonTapped:
52+
state.rows.append(NestedState(id: environment.uuid()))
5353
return .none
5454

55-
case .node:
55+
case let .nameTextFieldChanged(name):
56+
state.name = name
57+
return .none
58+
59+
case let .onDelete(indexSet):
60+
state.rows.remove(atOffsets: indexSet)
61+
return .none
62+
63+
case .row:
5664
return self.forEach(
57-
state: \.children,
58-
action: /NestedAction.node(id:action:),
65+
state: \.rows,
66+
action: /NestedAction.row(id:action:),
5967
environment: { $0 }
6068
)
6169
.run(&state, action, environment)
62-
63-
case let .remove(indexSet):
64-
state.children.remove(atOffsets: indexSet)
65-
return .none
66-
67-
case let .rename(name):
68-
state.description = name
69-
return .none
7070
}
7171
}
7272

7373
struct NestedView: View {
7474
let store: Store<NestedState, NestedAction>
7575

7676
var body: some View {
77-
WithViewStore(self.store, observe: \.description) { viewStore in
77+
WithViewStore(self.store, observe: \.name) { viewStore in
7878
Form {
7979
Section {
8080
AboutView(readMe: readMe)
8181
}
8282

8383
ForEachStore(
84-
self.store.scope(state: \.children, action: NestedAction.node(id:action:))
84+
self.store.scope(state: \.rows, action: NestedAction.row(id:action:))
8585
) { childStore in
86-
WithViewStore(childStore, observe: \.description) { childViewStore in
86+
WithViewStore(childStore, observe: \.name) { childViewStore in
8787
NavigationLink(
8888
destination: NestedView(store: childStore)
8989
) {
9090
HStack {
9191
TextField(
9292
"Untitled",
93-
text: childViewStore.binding(send: NestedAction.rename)
93+
text: childViewStore.binding(send: NestedAction.nameTextFieldChanged)
9494
)
9595
Text("Next")
9696
.font(.callout)
@@ -99,12 +99,12 @@ struct NestedView: View {
9999
}
100100
}
101101
}
102-
.onDelete { viewStore.send(.remove($0)) }
102+
.onDelete { viewStore.send(.onDelete($0)) }
103103
}
104104
.navigationTitle(viewStore.state.isEmpty ? "Untitled" : viewStore.state)
105105
.toolbar {
106106
ToolbarItem(placement: .navigationBarTrailing) {
107-
Button("Add row") { viewStore.send(.append) }
107+
Button("Add row") { viewStore.send(.addRowButtonTapped) }
108108
}
109109
}
110110
}
@@ -113,42 +113,26 @@ struct NestedView: View {
113113

114114
extension NestedState {
115115
static let mock = NestedState(
116-
children: [
117-
NestedState(
118-
children: [
119-
NestedState(
120-
children: [],
121-
id: UUID(),
122-
description: ""
123-
)
124-
],
125-
id: UUID(),
126-
description: "Bar"
127-
),
116+
id: UUID(),
117+
name: "Foo",
118+
rows: [
128119
NestedState(
129-
children: [
130-
NestedState(
131-
children: [],
132-
id: UUID(),
133-
description: "Fizz"
134-
),
135-
NestedState(
136-
children: [],
137-
id: UUID(),
138-
description: "Buzz"
139-
),
140-
],
141120
id: UUID(),
142-
description: "Baz"
121+
name: "Bar",
122+
rows: [
123+
NestedState(id: UUID(), name: "", rows: []),
124+
]
143125
),
144126
NestedState(
145-
children: [],
146127
id: UUID(),
147-
description: ""
128+
name: "Baz",
129+
rows: [
130+
NestedState(id: UUID(), name: "Fizz", rows: []),
131+
NestedState(id: UUID(), name: "Buzz", rows: []),
132+
]
148133
),
149-
],
150-
id: UUID(),
151-
description: "Foo"
134+
NestedState(id: UUID(), name: "", rows: []),
135+
]
152136
)
153137
}
154138

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import Combine
2+
import ComposableArchitecture
3+
import XCTest
4+
import XCTestDynamicOverlay
5+
6+
@testable import SwiftUICaseStudies
7+
8+
@MainActor
9+
final class RecursionTests: XCTestCase {
10+
func testAddRow() async {
11+
let store = TestStore(
12+
initialState: NestedState(id: UUID()),
13+
reducer: nestedReducer,
14+
environment: .unimplemented
15+
)
16+
17+
store.environment.uuid = UUID.incrementing
18+
19+
let id0 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
20+
let id1 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
21+
22+
await store.send(.addRowButtonTapped) {
23+
$0.rows.append(NestedState(id: id0))
24+
}
25+
26+
await store.send(.row(id: id0, action: .addRowButtonTapped)) {
27+
$0.rows[id: id0]?.rows.append(NestedState(id: id1))
28+
}
29+
}
30+
31+
func testChangeName() async {
32+
let store = TestStore(
33+
initialState: NestedState(id: UUID()),
34+
reducer: nestedReducer,
35+
environment: .unimplemented
36+
)
37+
38+
await store.send(.nameTextFieldChanged("Blob")) {
39+
$0.name = "Blob"
40+
}
41+
}
42+
43+
func testDeleteRow() async {
44+
let id0 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
45+
let id1 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
46+
let id2 = UUID(uuidString: "00000000-0000-0000-0000-000000000002")!
47+
48+
let store = TestStore(
49+
initialState: NestedState(
50+
id: UUID(),
51+
rows: [
52+
NestedState(id: id0),
53+
NestedState(id: id1),
54+
NestedState(id: id2),
55+
]
56+
),
57+
reducer: nestedReducer,
58+
environment: .unimplemented
59+
)
60+
61+
await store.send(.onDelete(IndexSet(integer: 1))) {
62+
$0.rows.remove(id: id1)
63+
}
64+
}
65+
}
66+
67+
extension NestedEnvironment {
68+
static let unimplemented = Self(
69+
uuid: XCTUnimplemented("UUID is unimplemented", placeholder: UUID())
70+
)
71+
}

0 commit comments

Comments
 (0)