Skip to content

Commit 108e3a5

Browse files
mbrandonwstephencelistgrapperon
authored
Concurrency Beta (pointfreeco#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 (pointfreeco#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 <[email protected]> * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <[email protected]> * docs and readme update * Update README.md * Update Tests/ComposableArchitectureTests/StoreTests.swift Co-authored-by: Stephen Celis <[email protected]> * fix * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <[email protected]> * clean up Co-authored-by: Stephen Celis <[email protected]> * 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 <[email protected]> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Effects/ConcurrencySupport.swift Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md Co-authored-by: Thomas Grapperon <[email protected]> * wip * wip * wip Co-authored-by: Stephen Celis <[email protected]> Co-authored-by: Thomas Grapperon <[email protected]>
1 parent 3f5f3c8 commit 108e3a5

File tree

152 files changed

+6986
-3693
lines changed

Some content is hidden

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

152 files changed

+6986
-3693
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-18
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: AnySchedulerOf<DispatchQueue>
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: .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,8 +279,7 @@ let rootReducer = Reducer<RootState, RootAction, RootEnvironment>.combine(
275279
.debug()
276280
.signpost()
277281

278-
private func liveFetchNumber() -> Effect<Int, Never> {
279-
Deferred { Just(Int.random(in: 1...1_000)) }
280-
.delay(for: 1, scheduler: DispatchQueue.main)
281-
.eraseToEffect()
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)
282285
}

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

+8-26
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Combine
22
import ComposableArchitecture
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,33 +19,14 @@ private let readMe = """
1919
toggle at the bottom of the screen.
2020
"""
2121

22-
extension Effect where Failure == Never {
23-
public static func keyFrames<S: Scheduler>(
24-
values: [(output: Output, duration: S.SchedulerTimeType.Stride)],
25-
scheduler: S
26-
) -> Self {
27-
.concatenate(
28-
values
29-
.enumerated()
30-
.map { index, animationState in
31-
index == 0
32-
? Effect(value: animationState.output)
33-
: Just(animationState.output)
34-
.delay(for: values[index - 1].duration, scheduler: scheduler)
35-
.eraseToEffect()
36-
}
37-
)
38-
}
39-
}
40-
4122
struct AnimationsState: Equatable {
4223
var alert: AlertState<AnimationsAction>?
4324
var circleCenter: CGPoint?
4425
var circleColor = Color.black
4526
var isCircleScaled = false
4627
}
4728

48-
enum AnimationsAction: Equatable {
29+
enum AnimationsAction: Equatable, Sendable {
4930
case alertDismissed
5031
case circleScaleToggleChanged(Bool)
5132
case rainbowButtonTapped
@@ -73,11 +54,12 @@ let animationsReducer = Reducer<AnimationsState, AnimationsAction, AnimationsEnv
7354
return .none
7455

7556
case .rainbowButtonTapped:
76-
return .keyFrames(
77-
values: [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black]
78-
.map { (output: .setColor($0), duration: 1) },
79-
scheduler: environment.mainQueue.animation(.linear)
80-
)
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: 1)
61+
}
62+
}
8163
.cancellable(id: CancelID.self)
8264

8365
case .resetButtonTapped:

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

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

3030
enum EffectsBasicsAction: Equatable {
3131
case decrementButtonTapped
32+
case decrementDelayResponse
3233
case incrementButtonTapped
3334
case numberFactButtonTapped
34-
case numberFactResponse(Result<String, FactClient.Failure>)
35+
case numberFactResponse(TaskResult<String>)
3536
}
3637

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

5571
case .incrementButtonTapped:
5672
state.count += 1
5773
state.numberFact = nil
58-
return .none
74+
return state.count >= 0
75+
? .cancel(id: DelayID.self)
76+
: .none
5977

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

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

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

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

1818
struct EffectsCancellationState: Equatable {
1919
var count = 0
20-
var currentTrivia: String?
21-
var isTriviaRequestInFlight = false
20+
var currentFact: String?
21+
var isFactRequestInFlight = false
2222
}
2323

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

3131
struct EffectsCancellationEnvironment {
3232
var fact: FactClient
33-
var mainQueue: AnySchedulerOf<DispatchQueue>
3433
}
3534

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

42-
enum TriviaRequestId {}
41+
enum NumberFactRequestID {}
4342

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

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

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

93-
if viewStore.isTriviaRequestInFlight {
92+
if viewStore.isFactRequestInFlight {
9493
HStack {
9594
Button("Cancel") { viewStore.send(.cancelButtonTapped) }
9695
Spacer()
@@ -100,11 +99,11 @@ struct EffectsCancellationView: View {
10099
.id(UUID())
101100
}
102101
} else {
103-
Button("Number fact") { viewStore.send(.triviaButtonTapped) }
104-
.disabled(viewStore.isTriviaRequestInFlight)
102+
Button("Number fact") { viewStore.send(.factButtonTapped) }
103+
.disabled(viewStore.isFactRequestInFlight)
105104
}
106105

107-
viewStore.currentTrivia.map {
106+
viewStore.currentFact.map {
108107
Text($0).padding(.vertical, 8)
109108
}
110109
}
@@ -133,8 +132,7 @@ struct EffectsCancellation_Previews: PreviewProvider {
133132
initialState: EffectsCancellationState(),
134133
reducer: effectsCancellationReducer,
135134
environment: EffectsCancellationEnvironment(
136-
fact: .live,
137-
mainQueue: .main
135+
fact: .live
138136
)
139137
)
140138
)

0 commit comments

Comments
 (0)