Skip to content

Commit 3688b3d

Browse files
Make Send sendable (#2112)
* Make Send wrap a sendable closure. * Move Send under the effects docs. * Fixed a bunch of doc references. * wip * wip * fix --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 5224148 commit 3688b3d

File tree

17 files changed

+41
-49
lines changed

17 files changed

+41
-49
lines changed

Sources/ComposableArchitecture/Documentation.docc/Articles/Bindings.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ struct Settings: ReducerProtocol {
232232
```
233233

234234
Binding actions are constructed and sent to the store by calling
235-
``ViewStore/binding(_:file:fileID:line:)`` with a key path to the binding state:
235+
``ViewStore/binding(_:fileID:line:)`` with a key path to the binding state:
236236

237237
```swift
238238
TextField("Display name", text: viewStore.binding(\.$displayName))

Sources/ComposableArchitecture/Documentation.docc/Articles/MigratingToTheReducerProtocol.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ A `Parent` reducer conformances can be made by implementing the
377377
``ReducerProtocol/body-swift.property-7foai`` property of the ``ReducerProtocol``, which allows you
378378
to express the parent's logic as a composition of multiple reducers. In particular, you can use
379379
the ``Reduce`` entry point to implement the core parent logic, and then chain on the
380-
``ReducerProtocol/ifLet(_:action:then:file:fileID:line:)`` operator to identify the optional child
380+
``ReducerProtocol/ifLet(_:action:then:fileID:line:)`` operator to identify the optional child
381381
state that you want to run the `Feature` reducer on when non-`nil`:
382382

383383
```swift
@@ -408,7 +408,7 @@ Because the `ifLet` operator has knowledge of both the parent and child reducers
408408
order to add an additional layer of correctness.
409409

410410
If you are using an enum to model your state, then there is a corresponding
411-
``ReducerProtocol/ifCaseLet(_:action:then:file:fileID:line:)`` operator that can help you run a
411+
``ReducerProtocol/ifCaseLet(_:action:then:fileID:line:)`` operator that can help you run a
412412
reducer on just one case of the enum.
413413

414414
## For-each reducers
@@ -417,7 +417,7 @@ Similar to `optional` reducers, another common pattern in applications is the us
417417
``AnyReducer/forEach(state:action:environment:file:fileID:line:)-2ypoa`` to allow running a reducer
418418
on each element of a collection. Converting such child and parent reducers will look nearly
419419
identical to what we did above for optional reducers, but it will make use of the new
420-
``ReducerProtocol/forEach(_:action:element:file:fileID:line:)`` operator instead.
420+
``ReducerProtocol/forEach(_:action:element:fileID:line:)`` operator instead.
421421

422422
In particular, the new `forEach` method operates on the parent reducer by specifying the collection
423423
sub-state you want to work on, and providing the element reducer you want to be able to run on

Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftConcurrency.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ for the time being, but in Swift 6 most (if not all) of these warnings will beco
88
you will need to know how to prove to the compiler that your types are safe to use concurrently.
99

1010
There primary way to create an ``EffectTask`` in the library is via
11-
``EffectPublisher/run(priority:operation:catch:file:fileID:line:)``. It takes a `@Sendable`,
11+
``EffectPublisher/run(priority:operation:catch:fileID:line:)``. It takes a `@Sendable`,
1212
asynchronous closure, which restricts the types of closures you can use for your effects. In
1313
particular, the closure can only capture `Sendable` variables that are bound with `let`. Mutable
1414
variables and non-`Sendable` types are simply not allowed to be passed to `@Sendable` closures.

Sources/ComposableArchitecture/Documentation.docc/Articles/Testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ Location, Core Motion, Speech Recognition, etc.), and more.
186186

187187
As a simple example, suppose we have a feature with a button such that when you tap it, it starts
188188
a timer that counts up until you reach 5, and then stops. This can be accomplished using the
189-
``EffectPublisher/run(priority:operation:catch:file:fileID:line:)`` helper on ``EffectTask``,
189+
``EffectPublisher/run(priority:operation:catch:fileID:line:)`` helper on ``EffectTask``,
190190
which provides you with an asynchronous context to operate in and can send multiple actions back
191191
into the system:
192192

Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/EffectDeprecations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Avoid using deprecated APIs in your app. Select a method to see the replacement
1515

1616
### Creating an effect
1717

18-
- ``EffectPublisher/task(priority:operation:catch:file:fileID:line:)``
18+
- ``EffectPublisher/task(priority:operation:catch:fileID:line:)``
1919
- ``EffectPublisher/task(priority:operation:)``
2020
- ``EffectPublisher/fireAndForget(priority:_:)``
2121

Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Creating an effect
66

77
- ``EffectPublisher/none``
8-
- ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)``
8+
- ``EffectPublisher/run(priority:operation:catch:fileID:line:)``
99
- ``EffectPublisher/send(_:)``
1010
- ``TaskResult``
1111

Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectRun.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ``ComposableArchitecture/EffectPublisher/run(priority:operation:catch:file:fileID:line:)``
1+
# ``ComposableArchitecture/EffectPublisher/run(priority:operation:catch:fileID:line:)``
22

33
## Topics
44

Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
- ``Body-swift.typealias``
1616
- ``ReducerBuilder``
1717
- ``Scope``
18-
- ``ifLet(_:action:then:file:fileID:line:)``
19-
- ``ifCaseLet(_:action:then:file:fileID:line:)``
20-
- ``forEach(_:action:element:file:fileID:line:)``
18+
- ``ifLet(_:action:then:fileID:line:)``
19+
- ``ifCaseLet(_:action:then:fileID:line:)``
20+
- ``forEach(_:action:element:fileID:line:)``
2121

2222
### Supporting reducers
2323

Sources/ComposableArchitecture/Documentation.docc/Extensions/SwiftUI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ The Composable Architecture can be used to power applications built in many fram
2323
- ``BindableAction``
2424
- ``BindingAction``
2525
- ``BindingReducer``
26-
- ``ViewStore/binding(_:file:fileID:line:)``
26+
- ``ViewStore/binding(_:fileID:line:)``
2727

2828
<!--DocC: Can't currently document `View` extensions-->
2929
<!--### View Modifiers-->

Sources/ComposableArchitecture/Effect.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ extension EffectPublisher {
9191
/// the other using Apple's Combine framework:
9292
///
9393
/// * If using Swift's native structured concurrency tools then there is one main way to create an
94-
/// effect: ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)``.
94+
/// effect: ``EffectPublisher/run(priority:operation:catch:fileID:line:)``.
9595
///
9696
/// * If using Combine in your application, in particular for the dependencies of your feature
9797
/// then you can create effects by making use of any of Combine's operators, and then erasing the
@@ -247,10 +247,10 @@ extension EffectPublisher where Failure == Never {
247247
///
248248
/// [callAsFunction]: https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID622
249249
@MainActor
250-
public struct Send<Action> {
251-
public let send: @MainActor (Action) -> Void
250+
public struct Send<Action>: Sendable {
251+
let send: @MainActor @Sendable (Action) -> Void
252252

253-
public init(send: @escaping @MainActor (Action) -> Void) {
253+
public init(send: @escaping @MainActor @Sendable (Action) -> Void) {
254254
self.send = send
255255
}
256256

Sources/ComposableArchitecture/Effects/Publisher.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ extension EffectPublisher: Publisher {
4646
let task = Task(priority: priority) { @MainActor in
4747
defer { subscriber.send(completion: .finished) }
4848
#if DEBUG
49-
var isCompleted = false
50-
defer { isCompleted = true }
49+
let isCompleted = LockIsolated(false)
50+
defer { isCompleted.setValue(true) }
5151
#endif
5252
let send = Send<Action> {
5353
#if DEBUG
54-
if isCompleted {
54+
if isCompleted.value {
5555
runtimeWarn(
5656
"""
5757
An action was sent from a completed effect:
@@ -86,7 +86,7 @@ extension EffectPublisher {
8686
///
8787
/// > Important: This Combine interface has been soft-deprecated in favor of Swift concurrency.
8888
/// > Prefer performing asynchronous work directly in
89-
/// > ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)`` by adopting a
89+
/// > ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` by adopting a
9090
/// > non-Combine interface, or by iterating over the publisher's asynchronous sequence of
9191
/// > `values`:
9292
/// >

Sources/ComposableArchitecture/Effects/TaskResult.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import XCTestDynamicOverlay
3131
/// }
3232
/// ```
3333
///
34-
/// And finally you can use ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)`` to
34+
/// And finally you can use ``EffectPublisher/run(priority:operation:catch:fileID:line:)`` to
3535
/// construct an effect in the reducer that invokes the `numberFact` endpoint and wraps its response
3636
/// in a ``TaskResult`` by using its catching initializer, ``TaskResult/init(catching:)``:
3737
///

Sources/ComposableArchitecture/Reducer/Reducers/Scope.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
/// ## Enum state
5959
///
6060
/// The ``Scope`` reducer also works when state is modeled as an enum, not just a struct. In that
61-
/// case you can use ``init(state:action:child:file:fileID:line:)`` to specify a case path that
61+
/// case you can use ``init(state:action:child:fileID:line:)`` to specify a case path that
6262
/// identifies the case of state you want to scope to.
6363
///
6464
/// For example, if your state was modeled as an enum for unloaded/loading/loaded, you could
@@ -94,7 +94,7 @@
9494
/// bugs, and so we show a runtime warning in that case, and cause test failures.
9595
///
9696
/// For an alternative to using ``Scope`` with state case paths that enforces the order, check out
97-
/// the ``ifCaseLet(_:action:then:file:fileID:line:)`` operator.
97+
/// the ``ifCaseLet(_:action:then:fileID:line:)`` operator.
9898
public struct Scope<ParentState, ParentAction, Child: ReducerProtocol>: ReducerProtocol {
9999
@usableFromInline
100100
enum StatePath {
@@ -199,7 +199,7 @@ public struct Scope<ParentState, ParentAction, Child: ReducerProtocol>: ReducerP
199199
/// > ```
200200
/// >
201201
/// > If the parent domain contains additional logic for switching between cases of child state,
202-
/// > prefer ``ReducerProtocol/ifCaseLet(_:action:then:file:fileID:line:)``, which better ensures
202+
/// > prefer ``ReducerProtocol/ifCaseLet(_:action:then:fileID:line:)``, which better ensures
203203
/// > that child logic runs _before_ any parent logic can replace child state:
204204
/// >
205205
/// > ```swift

Sources/ComposableArchitecture/Store.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,13 +414,13 @@ public final class Store<State, Action> {
414414
tasks.wrappedValue.append(
415415
Task(priority: priority) { @MainActor in
416416
#if DEBUG
417-
var isCompleted = false
418-
defer { isCompleted = true }
417+
let isCompleted = LockIsolated(false)
418+
defer { isCompleted.setValue(true) }
419419
#endif
420420
await operation(
421421
Send { effectAction in
422422
#if DEBUG
423-
if isCompleted {
423+
if isCompleted.value {
424424
runtimeWarn(
425425
"""
426426
An action was sent from a completed effect:

Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import SwiftUI
5656
/// }
5757
/// ```
5858
///
59-
/// Enhance its core reducer using ``ReducerProtocol/forEach(_:action:element:file:fileID:line:)``:
59+
/// Enhance its core reducer using ``ReducerProtocol/forEach(_:action:element:fileID:line:)``:
6060
///
6161
/// ```swift
6262
/// var body: some ReducerProtocol<State, Action> {

Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import SwiftUI
4141
/// > it changes. As such, you should not rely on this value for anything other than checking the
4242
/// > current case, _e.g._ by switching on it and routing to an appropriate `CaseLet`.
4343
///
44-
/// See ``ReducerProtocol/ifCaseLet(_:action:then:file:fileID:line:)`` and
45-
/// ``Scope/init(state:action:child:file:fileID:line:)`` for embedding reducers that operate on each
44+
/// See ``ReducerProtocol/ifCaseLet(_:action:then:fileID:line:)`` and
45+
/// ``Scope/init(state:action:child:fileID:line:)`` for embedding reducers that operate on each
4646
/// case of an enum in reducers that operate on the entire enum.
4747
public struct SwitchStore<State, Action, Content: View>: View {
4848
public let store: Store<State, Action>

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -662,11 +662,7 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme
662662
ScopedState: Equatable,
663663
Environment == Void
664664
{
665-
var dependencies = DependencyValues._current
666-
let reducer = withDependencies {
667-
prepareDependencies(&dependencies)
668-
$0 = dependencies
669-
} operation: {
665+
let reducer = withDependencies(prepareDependencies) {
670666
TestReducer(Reduce(reducer()), initialState: initialState())
671667
}
672668
self._environment = .init(wrappedValue: ())
@@ -677,7 +673,6 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme
677673
self.store = Store(initialState: reducer.state, reducer: reducer)
678674
self.timeout = 100 * NSEC_PER_MSEC
679675
self.toScopedState = toScopedState
680-
self.dependencies = dependencies
681676
}
682677

683678
/// Creates a test store with an initial state and a reducer powering its runtime.
@@ -703,11 +698,7 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme
703698
Action == ScopedAction,
704699
Environment == Void
705700
{
706-
var dependencies = DependencyValues._current
707-
let reducer = withDependencies {
708-
prepareDependencies(&dependencies)
709-
$0 = dependencies
710-
} operation: {
701+
let reducer = withDependencies(prepareDependencies) {
711702
TestReducer(Reduce(reducer()), initialState: initialState())
712703
}
713704
self._environment = .init(wrappedValue: ())
@@ -718,7 +709,6 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme
718709
self.store = Store(initialState: reducer.state, reducer: reducer)
719710
self.timeout = 100 * NSEC_PER_MSEC
720711
self.toScopedState = { $0 }
721-
self.dependencies = dependencies
722712
}
723713

724714
@available(
@@ -1177,7 +1167,7 @@ extension TestStore where ScopedState: Equatable {
11771167
var expectedWhenGivenPreviousState = expected
11781168
if let updateStateToExpectedResult = updateStateToExpectedResult {
11791169
try withDependencies {
1180-
$0 = self.dependencies
1170+
$0 = self.reducer.dependencies
11811171
} operation: {
11821172
try updateStateToExpectedResult(&expectedWhenGivenPreviousState)
11831173
}
@@ -1194,7 +1184,7 @@ extension TestStore where ScopedState: Equatable {
11941184
var expectedWhenGivenActualState = actual
11951185
if let updateStateToExpectedResult = updateStateToExpectedResult {
11961186
try withDependencies {
1197-
$0 = self.dependencies
1187+
$0 = self.reducer.dependencies
11981188
} operation: {
11991189
try updateStateToExpectedResult(&expectedWhenGivenActualState)
12001190
}
@@ -1213,7 +1203,7 @@ extension TestStore where ScopedState: Equatable {
12131203
_XCTExpectFailure(strict: false) {
12141204
do {
12151205
try withDependencies {
1216-
$0 = self.dependencies
1206+
$0 = self.reducer.dependencies
12171207
} operation: {
12181208
try updateStateToExpectedResult(&expectedWhenGivenPreviousState)
12191209
}
@@ -2164,8 +2154,8 @@ extension TestStore {
21642154
/// await store.send(.stopTimerButtonTapped).finish()
21652155
/// ```
21662156
///
2167-
/// See ``TestStore/finish(timeout:file:line:)`` for the ability to await all in-flight
2168-
/// effects in the test store.
2157+
/// See ``TestStore/finish(timeout:file:line:)`` for the ability to await all in-flight effects in
2158+
/// the test store.
21692159
///
21702160
/// See ``ViewStoreTask`` for the analog provided to ``ViewStore``.
21712161
public struct TestStoreTask: Hashable, Sendable {
@@ -2274,7 +2264,7 @@ public struct TestStoreTask: Hashable, Sendable {
22742264

22752265
class TestReducer<State, Action>: ReducerProtocol {
22762266
let base: Reduce<State, Action>
2277-
var dependencies = DependencyValues()
2267+
var dependencies: DependencyValues
22782268
let effectDidSubscribe = AsyncStream.makeStream(of: Void.self)
22792269
var inFlightEffects: Set<LongLivingEffect> = []
22802270
var receivedActions: [(action: Action, state: State)] = []
@@ -2284,7 +2274,9 @@ class TestReducer<State, Action>: ReducerProtocol {
22842274
_ base: Reduce<State, Action>,
22852275
initialState: State
22862276
) {
2277+
@Dependency(\.self) var dependencies
22872278
self.base = base
2279+
self.dependencies = dependencies
22882280
self.state = initialState
22892281
}
22902282

0 commit comments

Comments
 (0)