Skip to content

Commit 8688396

Browse files
Simplify effects basics case study. (#1166)
* Simplify effects basics case study. * wip * Update Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift Co-authored-by: Stephen Celis <[email protected]> * nb Co-authored-by: Stephen Celis <[email protected]>
1 parent d3768d6 commit 8688396

File tree

4 files changed

+77
-38
lines changed

4 files changed

+77
-38
lines changed

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,9 @@ private let readMe = """
1515
file access, socket connections, and anytime a scheduler is involved (such as debouncing, \
1616
throttling and delaying), and they are typically difficult to test.
1717
18-
This application has two simple side effects:
19-
20-
• Each time you count down the number will be incremented back up after a delay of 1 second.
21-
• Tapping "Number fact" will trigger an API request to load a piece of trivia about that number.
22-
23-
Both effects are handled by the reducer, and a full test suite is written to confirm that the \
24-
effects behave in the way we expect.
18+
This application has a simple side effect: tapping "Number fact" will trigger an API request to \
19+
load a piece of trivia about that number. This effect is handled by the reducer, and a full test \
20+
suite is written to confirm that the effect behaves in the way we expect.
2521
"""
2622

2723
// MARK: - Feature domain
@@ -47,15 +43,15 @@ struct EffectsBasicsEnvironment {
4743
// MARK: - Feature business logic
4844

4945
let effectsBasicsReducer = Reducer<
50-
EffectsBasicsState, EffectsBasicsAction, EffectsBasicsEnvironment
46+
EffectsBasicsState,
47+
EffectsBasicsAction,
48+
EffectsBasicsEnvironment
5149
> { state, action, environment in
5250
switch action {
5351
case .decrementButtonTapped:
5452
state.count -= 1
5553
state.numberFact = nil
56-
// Return an effect that re-increments the count after 1 second.
57-
return Effect(value: EffectsBasicsAction.incrementButtonTapped)
58-
.delay(1, on: environment.mainQueue)
54+
return .none
5955

6056
case .incrementButtonTapped:
6157
state.count += 1
@@ -77,6 +73,7 @@ let effectsBasicsReducer = Reducer<
7773
return .none
7874

7975
case .numberFactResponse(.failure):
76+
// NB: This is where we could handle the error is some way, such as showing an alert.
8077
state.isNumberFactRequestInFlight = false
8178
return .none
8279
}
@@ -90,15 +87,11 @@ struct EffectsBasicsView: View {
9087
var body: some View {
9188
WithViewStore(self.store) { viewStore in
9289
Form {
93-
Section(header: Text(readMe)) {
94-
EmptyView()
90+
Section {
91+
Text(readMe)
9592
}
9693

97-
Section(
98-
footer: Button("Number facts provided by numbersapi.com") {
99-
UIApplication.shared.open(URL(string: "http://numbersapi.com")!)
100-
}
101-
) {
94+
Section {
10295
HStack {
10396
Spacer()
10497
Button("") { viewStore.send(.decrementButtonTapped) }
@@ -110,15 +103,29 @@ struct EffectsBasicsView: View {
110103
.buttonStyle(.borderless)
111104

112105
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
106+
.frame(maxWidth: .infinity)
107+
113108
if viewStore.isNumberFactRequestInFlight {
114109
ProgressView()
110+
.frame(maxWidth: .infinity)
111+
// NB: There seems to be a bug in SwiftUI where the progress view does not show
112+
// a second time unless it is given a new identity.
113+
.id(UUID())
115114
}
116115

117116
if let numberFact = viewStore.numberFact {
118117
Text(numberFact)
119118
}
120119
}
120+
121+
Section {
122+
Button("Number facts provided by numbersapi.com") {
123+
UIApplication.shared.open(URL(string: "http://numbersapi.com")!)
124+
}
125+
.foregroundColor(.gray)
126+
}
121127
}
128+
.buttonStyle(.borderless)
122129
}
123130
.navigationBarTitle("Effects")
124131
}

Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,11 @@ struct EffectsCancellationView: View {
8181
var body: some View {
8282
WithViewStore(self.store) { viewStore in
8383
Form {
84-
Section(
85-
header: Text(readMe),
86-
footer: Button("Number facts provided by numbersapi.com") {
87-
UIApplication.shared.open(URL(string: "http://numbersapi.com")!)
88-
}
89-
) {
84+
Section {
85+
Text(readMe)
86+
}
87+
88+
Section {
9089
Stepper(
9190
value: viewStore.binding(
9291
get: \.count, send: EffectsCancellationAction.stepperChanged)
@@ -99,6 +98,9 @@ struct EffectsCancellationView: View {
9998
Button("Cancel") { viewStore.send(.cancelButtonTapped) }
10099
Spacer()
101100
ProgressView()
101+
// NB: There seems to be a bug in SwiftUI where the progress view does not show
102+
// a second time unless it is given a new identity.
103+
.id(UUID())
102104
}
103105
} else {
104106
Button("Number fact") { viewStore.send(.triviaButtonTapped) }
@@ -109,7 +111,15 @@ struct EffectsCancellationView: View {
109111
Text($0).padding(.vertical, 8)
110112
}
111113
}
114+
115+
Section {
116+
Button("Number facts provided by numbersapi.com") {
117+
UIApplication.shared.open(URL(string: "http://numbersapi.com")!)
118+
}
119+
.foregroundColor(.gray)
120+
}
112121
}
122+
.buttonStyle(.borderless)
113123
}
114124
.navigationBarTitle("Effect cancellation")
115125
}

Examples/CaseStudies/SwiftUICaseStudies/FactClient.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ extension FactClient {
1717
static let live = Self(
1818
fetch: { number in
1919
Effect.task {
20+
try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
2021
do {
2122
let (data, _) = try await URLSession.shared
2223
.data(from: URL(string: "http://numbersapi.com/\(number)/trivia")!)
2324
return String(decoding: data, as: UTF8.self)
2425
} catch {
25-
try? await Task.sleep(nanoseconds: NSEC_PER_SEC)
2626
return "\(number) is a good number Brent"
2727
}
2828
}
@@ -51,10 +51,7 @@ extension FactClient {
5151
// This is the "failing" fact dependency that is useful to plug into tests that you want
5252
// to prove do not need the dependency.
5353
static let failing = Self(
54-
fetch: { _ in
55-
XCTFail("\(Self.self).fact is unimplemented.")
56-
return .none
57-
}
54+
fetch: { _ in .failing("\(Self.self).fact is unimplemented.") }
5855
)
5956
}
6057
#endif

Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-BasicsTests.swift

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import XCTest
55
@testable import SwiftUICaseStudies
66

77
class EffectsBasicsTests: XCTestCase {
8-
func testCountDown() {
8+
func testCountUpAndDown() {
99
let store = TestStore(
1010
initialState: EffectsBasicsState(),
1111
reducer: effectsBasicsReducer,
@@ -21,21 +21,18 @@ class EffectsBasicsTests: XCTestCase {
2121
store.send(.decrementButtonTapped) {
2222
$0.count = 0
2323
}
24-
store.receive(.incrementButtonTapped) {
25-
$0.count = 1
26-
}
2724
}
2825

29-
func testNumberFact() {
26+
func testNumberFact_HappyPath() {
3027
let store = TestStore(
3128
initialState: EffectsBasicsState(),
3229
reducer: effectsBasicsReducer,
33-
environment: EffectsBasicsEnvironment(
34-
fact: FactClient(fetch: { n in Effect(value: "\(n) is a good number Brent") }),
35-
mainQueue: ImmediateScheduler()
36-
)
30+
environment: .failing
3731
)
3832

33+
store.environment.fact.fetch = { Effect(value: "\($0) is a good number Brent") }
34+
store.environment.mainQueue = .immediate
35+
3936
store.send(.incrementButtonTapped) {
4037
$0.count = 1
4138
}
@@ -47,4 +44,32 @@ class EffectsBasicsTests: XCTestCase {
4744
$0.numberFact = "1 is a good number Brent"
4845
}
4946
}
47+
48+
func testNumberFact_UnhappyPath() {
49+
let store = TestStore(
50+
initialState: EffectsBasicsState(),
51+
reducer: effectsBasicsReducer,
52+
environment: .failing
53+
)
54+
55+
store.environment.fact.fetch = { _ in Effect(error: FactClient.Error()) }
56+
store.environment.mainQueue = .immediate
57+
58+
store.send(.incrementButtonTapped) {
59+
$0.count = 1
60+
}
61+
store.send(.numberFactButtonTapped) {
62+
$0.isNumberFactRequestInFlight = true
63+
}
64+
store.receive(.numberFactResponse(.failure(FactClient.Error()))) {
65+
$0.isNumberFactRequestInFlight = false
66+
}
67+
}
68+
}
69+
70+
extension EffectsBasicsEnvironment {
71+
static let failing = Self(
72+
fact: .failing,
73+
mainQueue: .failing
74+
)
5075
}

0 commit comments

Comments
 (0)