Skip to content

Commit 103729d

Browse files
mbrandonwp4checo
authored andcommitted
Concurrency Beta (#1189)
* more main actor audit * wip * wip * fix * better task result == * task result tests * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix merge conflicts * wip * wip * lots of doc fixes and modernizations * lots more docs and better hashable conformance for TaskResult * more docs * clean up * more tests and docs * clean up * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * small clean up * wip * wip * wip * wip * wip * wip * wip * explicit * wip * fix bug in TestStore.receive * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fixes * wip * tools for non-deterministic TestStore.receive * fix * wip * wip * remove inAnyOrder stuff * wip * wip * wip * wip * wip * wip * wip * convert download case study to use async/await * animations * fix tests * remove executor experiment * wip * wip * wip * wip * wip * speech simplification * wip * wip * wip * wip * wip * wip * add a few todos * wrote some tests * simplify speech recognizer * fix tests * update some docs about error throwing behavior * wip * wip * fix * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Swift 5.5.2 fixes * wip * Bump timeout * wip * wip * Finesse * proper way to detect main queue * extra guard * revert main queue check * move stuff around * docs * fixed a bunch of warnings * Fix references * clean up * clean up * fix a bunch of warnings * clean up * un-soft deprecate concatenate * async teststore.send * fix uikit tests * drop sendable * wip * wip * wip * wip * wip * clean up * clean up * reorganize, remove extra task cancellation handler * wip * wip * wip * wip * wip * wip * Make TestStore.send async (#1190) * async teststore.send * fix uikit tests * Converted all tests to async * clean up * added docs * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <stephen@stephencelis.com> * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <stephen@stephencelis.com> * docs and readme update * Update README.md * Update Tests/ComposableArchitectureTests/StoreTests.swift Co-authored-by: Stephen Celis <stephen@stephencelis.com> * fix * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <stephen@stephencelis.com> * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <stephen@stephencelis.com> * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <stephen@stephencelis.com> * clean up Co-authored-by: Stephen Celis <stephen@stephencelis.com> * wip * wip * wip * make fetchNumber throwing and fix tests * effect basics clean up * use local state for isLoading in refreshable case study * clean up * fix test * wip * wip * wip * wip * wip * wip * fixes * clean up * clean up * Simplify * wip * clean up * wip * AsyncStream.finished() * give Send a public initializer * make send public * temporarily make box public * remove concurrency flag * wip * wip * wip * wip * wip * docs * speech * simplify * clean up; * unchecked sendable * clean up * clean up * wip * docs * docs * more docs * lots of docs * wip * wip * wip * more docs for streamWithContinuation * wip * wip * wip * Make internal, too * wip * Remove sendability detection It breaks things, like: let request = UncheckedSendable( SKProductsRequest(productIdentifiers: [] ) // UncheckedSendable<NSObject> // *not* _<SKProductsRequest> * wip * doc clean up; * fixed some todos * docs * wip * remove thread safety FAQ from readme * fix test * wip * docs clean up * docs clean up * added a testing article and fixed some docs * rearrange * docs clean up * wip * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> * wip * wip * wip Co-authored-by: Stephen Celis <stephen@stephencelis.com> Co-authored-by: Thomas Grapperon <35562418+tgrapperon@users.noreply.github.com> (cherry picked from commit 108e3a536fcebb16c4f247ef92c2d7326baf9fe3) # Conflicts: # Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift # Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-LongLiving.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Refreshable.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-SystemEnvironment.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Timers.swift # Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift # Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-LoadThenNavigate.swift # Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-NavigateAndLoad.swift # Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-LoadThenNavigate.swift # Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-NavigateAndLoad.swift # Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-LoadThenPresent.swift # Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-PresentAndLoad.swift # Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift # Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift # Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift # Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift # Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift # Examples/CaseStudies/SwiftUICaseStudies/FactClient.swift # Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AnimationsTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-BasicsTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-CancellationTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-RefreshableTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-TimersTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift # Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift # Examples/CaseStudies/UIKitCaseStudies/LoadThenNavigate.swift # Examples/CaseStudies/UIKitCaseStudies/NavigateAndLoad.swift # Examples/Search/Search/SearchView.swift # Examples/Search/Search/WeatherClient.swift # Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift # Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift # Examples/SpeechRecognition/SpeechRecognition/SpeechRecognitionApp.swift # Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift # Examples/TicTacToe/App/RootView.swift # Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift # Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClientLive/LiveAuthenticationClient.swift # Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift # Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift # Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift # Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift # Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift # Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift # Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift # Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift # Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift # Examples/Todos/Todos/Todos.swift # Examples/Todos/Todos/TodosApp.swift # Examples/Todos/TodosTests/TodosTests.swift # Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift # Examples/VoiceMemos/VoiceMemos/VoiceMemo.swift # Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift # Examples/VoiceMemos/VoiceMemos/VoiceMemosApp.swift # Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift # Package.swift # README.md # Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md # Sources/ComposableArchitecture/Effect.swift # Sources/ComposableArchitecture/Effects/Animation.swift # Sources/ComposableArchitecture/Effects/Cancellation.swift # Sources/ComposableArchitecture/Effects/Concurrency.swift # Sources/ComposableArchitecture/Effects/Timer.swift # Sources/ComposableArchitecture/Internal/Create.swift # Sources/ComposableArchitecture/Internal/Deprecations.swift # Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift # Sources/ComposableArchitecture/Reducer.swift # Sources/ComposableArchitecture/Store.swift # Sources/ComposableArchitecture/SwiftUI/Alert.swift # Sources/ComposableArchitecture/SwiftUI/Binding.swift # Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift # Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift # Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift # Sources/ComposableArchitecture/TestSupport/TestStore.swift # Sources/ComposableArchitecture/ViewStore.swift # Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift # Tests/ComposableArchitectureTests/EffectDebounceTests.swift # Tests/ComposableArchitectureTests/EffectTests.swift # Tests/ComposableArchitectureTests/ReducerTests.swift # Tests/ComposableArchitectureTests/RuntimeWarningTests.swift # Tests/ComposableArchitectureTests/StoreTests.swift # Tests/ComposableArchitectureTests/TestStoreFailureTests.swift # Tests/ComposableArchitectureTests/TestStoreTests.swift # Tests/ComposableArchitectureTests/TimerTests.swift # Tests/ComposableArchitectureTests/ViewStoreTests.swift
1 parent aecd2a5 commit 103729d

File tree

158 files changed

+7572
-3724
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

158 files changed

+7572
-3724
lines changed

Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,7 @@
930930
"$(inherited)",
931931
"@executable_path/Frameworks",
932932
);
933+
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks";
933934
PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.UIKitCaseStudies;
934935
PRODUCT_NAME = "$(TARGET_NAME)";
935936
SWIFT_VERSION = 5.0;
@@ -949,6 +950,7 @@
949950
"$(inherited)",
950951
"@executable_path/Frameworks",
951952
);
953+
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks";
952954
PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.UIKitCaseStudies;
953955
PRODUCT_NAME = "$(TARGET_NAME)";
954956
SWIFT_VERSION = 5.0;
@@ -1121,6 +1123,7 @@
11211123
"$(inherited)",
11221124
"@executable_path/Frameworks",
11231125
);
1126+
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks";
11241127
PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUICaseStudies;
11251128
PRODUCT_NAME = "$(TARGET_NAME)";
11261129
SWIFT_VERSION = 5.0;
@@ -1139,6 +1142,7 @@
11391142
"$(inherited)",
11401143
"@executable_path/Frameworks",
11411144
);
1145+
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks";
11421146
PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUICaseStudies;
11431147
PRODUCT_NAME = "$(TARGET_NAME)";
11441148
SWIFT_VERSION = 5.0;

Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift

+21-17
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,31 @@ enum RootAction {
6666
}
6767

6868
struct RootEnvironment {
69-
var date: () -> Date
69+
var date: @Sendable () -> Date
7070
var downloadClient: DownloadClient
7171
var fact: FactClient
72-
var favorite: (UUID, Bool) -> Effect<Bool, Error>
73-
var fetchNumber: () -> Effect<Int, Never>
72+
var favorite: @Sendable (UUID, Bool) async throws -> Bool
73+
var fetchNumber: @Sendable () async throws -> Int
7474
var mainQueue: DateScheduler
75-
var notificationCenter: NotificationCenter
76-
var uuid: () -> UUID
75+
var screenshots: @Sendable () async -> AsyncStream<Void>
76+
var uuid: @Sendable () -> UUID
7777
var webSocket: WebSocketClient
7878

7979
static let live = Self(
80-
date: Date.init,
80+
date: { Date() },
8181
downloadClient: .live,
8282
fact: .live,
8383
favorite: favorite(id:isFavorite:),
8484
fetchNumber: liveFetchNumber,
8585
mainQueue: QueueScheduler.main,
86-
notificationCenter: .default,
87-
uuid: UUID.init,
86+
screenshots: { @MainActor in
87+
AsyncStream(
88+
NotificationCenter.default
89+
.notifications(named: UIApplication.userDidTakeScreenshotNotification)
90+
.map { _ in }
91+
)
92+
},
93+
uuid: { UUID() },
8894
webSocket: .live
8995
)
9096
}
@@ -146,13 +152,13 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
146152
.pullback(
147153
state: \.effectsCancellation,
148154
action: /RootAction.effectsCancellation,
149-
environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) }
155+
environment: { .init(fact: $0.fact) }
150156
),
151157
episodesReducer
152158
.pullback(
153159
state: \.episodes,
154160
action: /RootAction.episodes,
155-
environment: { .init(favorite: $0.favorite, mainQueue: $0.mainQueue) }
161+
environment: { .init(favorite: $0.favorite) }
156162
),
157163
focusDemoReducer
158164
.pullback(
@@ -188,7 +194,7 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
188194
.pullback(
189195
state: \.longLivingEffects,
190196
action: /RootAction.longLivingEffects,
191-
environment: { .init(notificationCenter: $0.notificationCenter) }
197+
environment: { .init(screenshots: $0.screenshots) }
192198
),
193199
mapAppReducer
194200
.pullback(
@@ -243,9 +249,7 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
243249
.pullback(
244250
state: \.refreshable,
245251
action: /RootAction.refreshable,
246-
environment: {
247-
.init(fact: $0.fact, mainQueue: $0.mainQueue)
248-
}
252+
environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) }
249253
),
250254
sharedStateReducer
251255
.pullback(
@@ -275,7 +279,7 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
275279
.debug()
276280
.signpost()
277281

278-
private func liveFetchNumber() -> Effect<Int, Never> {
279-
Effect.deferred { Effect(value: Int.random(in: 1...1_000)) }
280-
.delay(1, on: QueueScheduler.main)
282+
@Sendable private func liveFetchNumber() async throws -> Int {
283+
try await Task.sleep(nanoseconds: NSEC_PER_SEC)
284+
return Int.random(in: 1...1_000)
281285
}

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

+8-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ComposableArchitecture
22
import ReactiveSwift
3-
import SwiftUI
3+
@preconcurrency import SwiftUI // NB: SwiftUI.Color and SwiftUI.Animation are not Sendable yet.
44

55
private let readMe = """
66
This screen demonstrates how changes to application state can drive animations. Because the \
@@ -19,32 +19,14 @@ private let readMe = """
1919
toggle at the bottom of the screen.
2020
"""
2121

22-
extension Effect where Error == Never {
23-
public static func keyFrames(
24-
values: [(output: Value, duration: TimeInterval)],
25-
scheduler: DateScheduler
26-
) -> Self {
27-
.concatenate(
28-
values
29-
.enumerated()
30-
.map { index, animationState in
31-
index == 0
32-
? Effect(value: animationState.output)
33-
: Effect(value: animationState.output)
34-
.delay(values[index - 1].duration, on: scheduler)
35-
}
36-
)
37-
}
38-
}
39-
4022
struct AnimationsState: Equatable {
4123
var alert: AlertState<AnimationsAction>?
4224
var circleCenter: CGPoint?
4325
var circleColor = Color.black
4426
var isCircleScaled = false
4527
}
4628

47-
enum AnimationsAction: Equatable {
29+
enum AnimationsAction: Equatable, Sendable {
4830
case alertDismissed
4931
case circleScaleToggleChanged(Bool)
5032
case rainbowButtonTapped
@@ -72,11 +54,12 @@ let animationsReducer = Reducer<AnimationsState, AnimationsAction, AnimationsEnv
7254
return .none
7355

7456
case .rainbowButtonTapped:
75-
return .keyFrames(
76-
values: [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black]
77-
.map { (output: .setColor($0), duration: 1) },
78-
scheduler: environment.mainQueue.animation(.linear)
79-
)
57+
return .run { send in
58+
for color in [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black] {
59+
await send(.setColor(color), animation: .linear)
60+
try await environment.mainQueue.sleep(for: .seconds(1))
61+
}
62+
}
8063
.cancellable(id: CancelID.self)
8164

8265
case .resetButtonTapped:

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

+23-5
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ struct EffectsBasicsState: Equatable {
3030

3131
enum EffectsBasicsAction: Equatable {
3232
case decrementButtonTapped
33+
case decrementDelayResponse
3334
case incrementButtonTapped
3435
case numberFactButtonTapped
35-
case numberFactResponse(Result<String, FactClient.Failure>)
36+
case numberFactResponse(TaskResult<String>)
3637
}
3738

3839
struct EffectsBasicsEnvironment {
@@ -47,25 +48,42 @@ let effectsBasicsReducer = Reducer<
4748
EffectsBasicsAction,
4849
EffectsBasicsEnvironment
4950
> { state, action, environment in
51+
enum DelayID {}
52+
5053
switch action {
5154
case .decrementButtonTapped:
5255
state.count -= 1
5356
state.numberFact = nil
57+
// Return an effect that re-increments the count after 1 second if the count is negative
58+
return state.count >= 0
59+
? .none
60+
: .task {
61+
try await environment.mainQueue.sleep(for: .seconds(1))
62+
return .decrementDelayResponse
63+
}
64+
.cancellable(id: DelayID.self)
65+
66+
case .decrementDelayResponse:
67+
if state.count < 0 {
68+
state.count += 1
69+
}
5470
return .none
5571

5672
case .incrementButtonTapped:
5773
state.count += 1
5874
state.numberFact = nil
59-
return .none
75+
return state.count >= 0
76+
? .cancel(id: DelayID.self)
77+
: .none
6078

6179
case .numberFactButtonTapped:
6280
state.isNumberFactRequestInFlight = true
6381
state.numberFact = nil
6482
// Return an effect that fetches a number fact from the API and returns the
6583
// value back to the reducer's `numberFactResponse` action.
66-
return environment.fact.fetch(state.count)
67-
.observe(on: environment.mainQueue)
68-
.catchToEffect(EffectsBasicsAction.numberFactResponse)
84+
return .task { [count = state.count] in
85+
await .numberFactResponse(TaskResult { try await environment.fact.fetch(count) })
86+
}
6987

7088
case let .numberFactResponse(.success(response)):
7189
state.isNumberFactRequestInFlight = false

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

+30-32
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,19 @@ private let readMe = """
1818

1919
struct EffectsCancellationState: Equatable {
2020
var count = 0
21-
var currentTrivia: String?
22-
var isTriviaRequestInFlight = false
21+
var currentFact: String?
22+
var isFactRequestInFlight = false
2323
}
2424

2525
enum EffectsCancellationAction: Equatable {
2626
case cancelButtonTapped
2727
case stepperChanged(Int)
28-
case triviaButtonTapped
29-
case triviaResponse(Result<String, FactClient.Failure>)
28+
case factButtonTapped
29+
case factResponse(TaskResult<String>)
3030
}
3131

3232
struct EffectsCancellationEnvironment {
3333
var fact: FactClient
34-
var mainQueue: DateScheduler
3534
}
3635

3736
// MARK: - Business logic
@@ -40,35 +39,35 @@ let effectsCancellationReducer = Reducer<
4039
EffectsCancellationState, EffectsCancellationAction, EffectsCancellationEnvironment
4140
> { state, action, environment in
4241

43-
enum TriviaRequestId {}
42+
enum NumberFactRequestID {}
4443

4544
switch action {
4645
case .cancelButtonTapped:
47-
state.isTriviaRequestInFlight = false
48-
return .cancel(id: TriviaRequestId.self)
46+
state.isFactRequestInFlight = false
47+
return .cancel(id: NumberFactRequestID.self)
4948

5049
case let .stepperChanged(value):
5150
state.count = value
52-
state.currentTrivia = nil
53-
state.isTriviaRequestInFlight = false
54-
return .cancel(id: TriviaRequestId.self)
55-
56-
case .triviaButtonTapped:
57-
state.currentTrivia = nil
58-
state.isTriviaRequestInFlight = true
59-
60-
return environment.fact.fetch(state.count)
61-
.observe(on: environment.mainQueue)
62-
.catchToEffect(EffectsCancellationAction.triviaResponse)
63-
.cancellable(id: TriviaRequestId.self)
64-
65-
case let .triviaResponse(.success(response)):
66-
state.isTriviaRequestInFlight = false
67-
state.currentTrivia = response
51+
state.currentFact = nil
52+
state.isFactRequestInFlight = false
53+
return .cancel(id: NumberFactRequestID.self)
54+
55+
case .factButtonTapped:
56+
state.currentFact = nil
57+
state.isFactRequestInFlight = true
58+
59+
return .task { [count = state.count] in
60+
await .factResponse(TaskResult { try await environment.fact.fetch(count) })
61+
}
62+
.cancellable(id: NumberFactRequestID.self)
63+
64+
case let .factResponse(.success(response)):
65+
state.isFactRequestInFlight = false
66+
state.currentFact = response
6867
return .none
6968

70-
case .triviaResponse(.failure):
71-
state.isTriviaRequestInFlight = false
69+
case .factResponse(.failure):
70+
state.isFactRequestInFlight = false
7271
return .none
7372
}
7473
}
@@ -91,7 +90,7 @@ struct EffectsCancellationView: View {
9190
value: viewStore.binding(get: \.count, send: EffectsCancellationAction.stepperChanged)
9291
)
9392

94-
if viewStore.isTriviaRequestInFlight {
93+
if viewStore.isFactRequestInFlight {
9594
HStack {
9695
Button("Cancel") { viewStore.send(.cancelButtonTapped) }
9796
Spacer()
@@ -101,11 +100,11 @@ struct EffectsCancellationView: View {
101100
.id(UUID())
102101
}
103102
} else {
104-
Button("Number fact") { viewStore.send(.triviaButtonTapped) }
105-
.disabled(viewStore.isTriviaRequestInFlight)
103+
Button("Number fact") { viewStore.send(.factButtonTapped) }
104+
.disabled(viewStore.isFactRequestInFlight)
106105
}
107106

108-
viewStore.currentTrivia.map {
107+
viewStore.currentFact.map {
109108
Text($0).padding(.vertical, 8)
110109
}
111110
}
@@ -134,8 +133,7 @@ struct EffectsCancellation_Previews: PreviewProvider {
134133
initialState: EffectsCancellationState(),
135134
reducer: effectsCancellationReducer,
136135
environment: EffectsCancellationEnvironment(
137-
fact: .live,
138-
mainQueue: QueueScheduler.main
136+
fact: .live
139137
)
140138
)
141139
)

0 commit comments

Comments
 (0)