Skip to content

Commit 6922a82

Browse files
committed
Extract number fact dependency to a FactClient. (#600)
* Extract number fact dependency to a FactClient. * wip
1 parent 6738d17 commit 6922a82

File tree

8 files changed

+72
-45
lines changed

8 files changed

+72
-45
lines changed

Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */; };
1818
CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */; };
1919
CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */; };
20+
CA5ECF92267A79F0002067FF /* FactClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5ECF91267A79F0002067FF /* FactClient.swift */; };
2021
CA6AC2642451135C00C71CB3 /* ReusableComponents-Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */; };
2122
CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */; };
2223
CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */; };
@@ -157,6 +158,7 @@
157158
CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = "<group>"; };
158159
CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = "<group>"; };
159160
CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndActionSheetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndActionSheetsTests.swift"; sourceTree = "<group>"; };
161+
CA5ECF91267A79F0002067FF /* FactClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactClient.swift; sourceTree = "<group>"; };
160162
CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReusableComponents-Download.swift"; sourceTree = "<group>"; };
161163
CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
162164
CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadComponent.swift; sourceTree = "<group>"; };
@@ -413,6 +415,7 @@
413415
DCE63B70245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift */,
414416
DCC68EE22447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift */,
415417
DC2E370C24573ACB00B94699 /* 04-HigherOrderReducers-StrictReducers.swift */,
418+
CA5ECF91267A79F0002067FF /* FactClient.swift */,
416419
DC89C41824460F95006900B9 /* SceneDelegate.swift */,
417420
DC89C41C24460F96006900B9 /* Assets.xcassets */,
418421
CA6AC25F2451131C00C71CB3 /* 04-HigherOrderReducers-ResuableOfflineDownloads */,
@@ -749,6 +752,7 @@
749752
DC89C44724461431006900B9 /* ActivityIndicator.swift in Sources */,
750753
CAA9ADC624465C810003A984 /* 02-Effects-Cancellation.swift in Sources */,
751754
DC2E370D24573ACB00B94699 /* 04-HigherOrderReducers-StrictReducers.swift in Sources */,
755+
CA5ECF92267A79F0002067FF /* FactClient.swift in Sources */,
752756
DC9EB4172450CBD2005F413B /* UIViewRepresented.swift in Sources */,
753757
DC89C41924460F95006900B9 /* SceneDelegate.swift in Sources */,
754758
CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */,

Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import ComposableArchitecture
22
import ReactiveSwift
33
import UIKit
4+
import XCTestDynamicOverlay
45

56
struct RootState {
67
var alertAndActionSheet = AlertAndSheetState()
@@ -65,21 +66,21 @@ enum RootAction {
6566
struct RootEnvironment {
6667
var date: () -> Date
6768
var downloadClient: DownloadClient
69+
var fact: FactClient
6870
var favorite: (UUID, Bool) -> Effect<Bool, Error>
6971
var fetchNumber: () -> Effect<Int, Never>
7072
var mainQueue: DateScheduler
71-
var numberFact: (Int) -> Effect<String, NumbersApiError>
7273
var userDidTakeScreenshot: Effect<Void, Never>
7374
var uuid: () -> UUID
7475
var webSocket: WebSocketClient
7576

7677
static let live = Self(
7778
date: Date.init,
7879
downloadClient: .live,
80+
fact: .live,
7981
favorite: favorite(id:isFavorite:),
8082
fetchNumber: liveFetchNumber,
8183
mainQueue: QueueScheduler.main,
82-
numberFact: liveNumberFact(for:),
8384
userDidTakeScreenshot: liveUserDidTakeScreenshot.producer,
8485
uuid: UUID.init,
8586
webSocket: .live
@@ -143,13 +144,13 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
143144
.pullback(
144145
state: \.effectsBasics,
145146
action: /RootAction.effectsBasics,
146-
environment: { .init(mainQueue: $0.mainQueue, numberFact: $0.numberFact) }
147+
environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) }
147148
),
148149
effectsCancellationReducer
149150
.pullback(
150151
state: \.effectsCancellation,
151152
action: /RootAction.effectsCancellation,
152-
environment: { .init(mainQueue: $0.mainQueue, numberFact: $0.numberFact) }
153+
environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) }
153154
),
154155
episodesReducer
155156
.pullback(
@@ -263,21 +264,6 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
263264
)
264265
.signpost()
265266

266-
// This is the "live" trivia dependency that reaches into the outside world to fetch trivia.
267-
// Typically this live implementation of the dependency would live in its own module so that the
268-
// main feature doesn't need to compile it.
269-
func liveNumberFact(for n: Int) -> Effect<String, NumbersApiError> {
270-
URLSession.shared.reactive.data(
271-
with: URLRequest(url: URL(string: "http://numbersapi.com/\(n)/trivia")!)
272-
)
273-
.map { data, _ in String(decoding: data, as: UTF8.self) }
274-
.flatMapError { _ in
275-
Effect(value: "\(n) is a good number Brent")
276-
.delay(1, on: QueueScheduler.main)
277-
}
278-
.promoteError(NumbersApiError.self)
279-
}
280-
281267
private func liveFetchNumber() -> Effect<Int, Never> {
282268
Effect.deferred { Effect(value: Int.random(in: 1...1_000)) }
283269
.delay(1, on: QueueScheduler.main)

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,12 @@ enum EffectsBasicsAction: Equatable {
3636
case decrementButtonTapped
3737
case incrementButtonTapped
3838
case numberFactButtonTapped
39-
case numberFactResponse(Result<String, NumbersApiError>)
39+
case numberFactResponse(Result<String, FactClient.Error>)
4040
}
4141

42-
struct NumbersApiError: Error, Equatable {}
43-
4442
struct EffectsBasicsEnvironment {
43+
var fact: FactClient
4544
var mainQueue: DateScheduler
46-
var numberFact: (Int) -> Effect<String, NumbersApiError>
4745
}
4846

4947
// MARK: - Feature business logic
@@ -69,7 +67,7 @@ let effectsBasicsReducer = Reducer<
6967
state.numberFact = nil
7068
// Return an effect that fetches a number fact from the API and returns the
7169
// value back to the reducer's `numberFactResponse` action.
72-
return environment.numberFact(state.count)
70+
return environment.fact.fetch(state.count)
7371
.observe(on: environment.mainQueue)
7472
.catchToEffect()
7573
.map(EffectsBasicsAction.numberFactResponse)
@@ -135,8 +133,9 @@ struct EffectsBasicsView_Previews: PreviewProvider {
135133
initialState: EffectsBasicsState(),
136134
reducer: effectsBasicsReducer,
137135
environment: EffectsBasicsEnvironment(
138-
mainQueue: QueueScheduler.main,
139-
numberFact: liveNumberFact(for:))
136+
fact: .live,
137+
mainQueue: QueueScheduler.main
138+
)
140139
)
141140
)
142141
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ enum EffectsCancellationAction: Equatable {
2626
case cancelButtonTapped
2727
case stepperChanged(Int)
2828
case triviaButtonTapped
29-
case triviaResponse(Result<String, NumbersApiError>)
29+
case triviaResponse(Result<String, FactClient.Error>)
3030
}
3131

3232
struct EffectsCancellationEnvironment {
33+
var fact: FactClient
3334
var mainQueue: DateScheduler
34-
var numberFact: (Int) -> Effect<String, NumbersApiError>
3535
}
3636

3737
// MARK: - Business logic
@@ -57,7 +57,7 @@ let effectsCancellationReducer = Reducer<
5757
state.currentTrivia = nil
5858
state.isTriviaRequestInFlight = true
5959

60-
return environment.numberFact(state.count)
60+
return environment.fact.fetch(state.count)
6161
.observe(on: environment.mainQueue)
6262
.catchToEffect()
6363
.map(EffectsCancellationAction.triviaResponse)
@@ -126,8 +126,8 @@ struct EffectsCancellation_Previews: PreviewProvider {
126126
initialState: EffectsCancellationState(),
127127
reducer: effectsCancellationReducer,
128128
environment: EffectsCancellationEnvironment(
129-
mainQueue: QueueScheduler.main,
130-
numberFact: liveNumberFact(for:)
129+
fact: .live,
130+
mainQueue: QueueScheduler.main
131131
)
132132
)
133133
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
import ReactiveSwift
3+
import ComposableArchitecture
4+
import XCTestDynamicOverlay
5+
6+
struct FactClient {
7+
var fetch: (Int) -> Effect<String, Error>
8+
9+
struct Error: Swift.Error, Equatable {}
10+
}
11+
12+
extension FactClient {
13+
// This is the "live" fact dependency that reaches into the outside world to fetch trivia.
14+
// Typically this live implementation of the dependency would live in its own module so that the
15+
// main feature doesn't need to compile it.
16+
static let live = Self(
17+
fetch: { number in
18+
URLSession.shared.reactive.data(
19+
with: URLRequest(url: URL(string: "http://numbersapi.com/\(number)/trivia")!)
20+
)
21+
.map { data, _ in String(decoding: data, as: UTF8.self) }
22+
.flatMapError { _ in
23+
Effect(value: "\(number) is a good number Brent")
24+
.delay(1, on: QueueScheduler.main)
25+
}
26+
.promoteError(Error.self)
27+
})
28+
}
29+
30+
extension FactClient {
31+
// This is the "unimplemented" fact dependency that is useful to plug into tests that you want
32+
// to prove do not need the dependency.
33+
static let unimplemented = Self(
34+
fetch: { _ in
35+
XCTFail("\(Self.self).fact is unimplemented.")
36+
return .none
37+
})
38+
}

Examples/CaseStudies/SwiftUICaseStudies/Internal/CircularProgressView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ struct CircularProgressView: View {
1313
.stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round))
1414
.foregroundColor(Color.black)
1515
.rotationEffect(Angle(degrees: -90))
16-
.animation(.easeIn)
16+
.animation(.easeIn, value: self.value)
1717
}
1818
}
1919

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class EffectsBasicsTests: XCTestCase {
1010
initialState: EffectsBasicsState(),
1111
reducer: effectsBasicsReducer,
1212
environment: EffectsBasicsEnvironment(
13-
mainQueue: ImmediateScheduler(),
14-
numberFact: { _ in fatalError("Unimplemented") }
13+
fact: .unimplemented,
14+
mainQueue: ImmediateScheduler()
1515
)
1616
)
1717

@@ -31,8 +31,8 @@ class EffectsBasicsTests: XCTestCase {
3131
initialState: EffectsBasicsState(),
3232
reducer: effectsBasicsReducer,
3333
environment: EffectsBasicsEnvironment(
34-
mainQueue: ImmediateScheduler(),
35-
numberFact: { n in Effect(value: "\(n) is a good number Brent") }
34+
fact: .init(fetch: { n in Effect(value: "\(n) is a good number Brent") }),
35+
mainQueue: ImmediateScheduler()
3636
)
3737
)
3838

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class EffectsCancellationTests: XCTestCase {
1010
initialState: .init(),
1111
reducer: effectsCancellationReducer,
1212
environment: .init(
13-
mainQueue: ImmediateScheduler(),
14-
numberFact: { n in Effect(value: "\(n) is a good number Brent") }
13+
fact: .init(fetch: { n in Effect(value: "\(n) is a good number Brent") }),
14+
mainQueue: ImmediateScheduler()
1515
)
1616
)
1717

@@ -35,15 +35,15 @@ class EffectsCancellationTests: XCTestCase {
3535
initialState: .init(),
3636
reducer: effectsCancellationReducer,
3737
environment: .init(
38-
mainQueue: ImmediateScheduler(),
39-
numberFact: { _ in Effect(error: NumbersApiError()) }
38+
fact: .init(fetch: { _ in Fail(error: FactClient.Error()).eraseToEffect() }),
39+
mainQueue: ImmediateScheduler()
4040
)
4141
)
4242

4343
store.send(.triviaButtonTapped) {
4444
$0.isTriviaRequestInFlight = true
4545
}
46-
store.receive(.triviaResponse(.failure(NumbersApiError()))) {
46+
store.receive(.triviaResponse(.failure(FactClient.Error()))) {
4747
$0.isTriviaRequestInFlight = false
4848
}
4949
}
@@ -60,8 +60,8 @@ class EffectsCancellationTests: XCTestCase {
6060
initialState: .init(),
6161
reducer: effectsCancellationReducer,
6262
environment: .init(
63-
mainQueue: scheduler,
64-
numberFact: { n in Effect(value: "\(n) is a good number Brent") }
63+
fact: .init(fetch: { n in Effect(value: "\(n) is a good number Brent") }),
64+
mainQueue: scheduler
6565
)
6666
)
6767

@@ -80,8 +80,8 @@ class EffectsCancellationTests: XCTestCase {
8080
initialState: .init(),
8181
reducer: effectsCancellationReducer,
8282
environment: .init(
83-
mainQueue: scheduler,
84-
numberFact: { n in Effect(value: "\(n) is a good number Brent") }
83+
fact: .init(fetch: { n in Effect(value: "\(n) is a good number Brent") }),
84+
mainQueue: scheduler
8585
)
8686
)
8787

0 commit comments

Comments
 (0)