Skip to content

Commit 084c6bd

Browse files
stephencelisp4checo
authored andcommitted
Add Effect.send (#1859)
* Add `Effect.send` With the `Effect<Action, Failure>` -> `Effect<Action>` migration, `Effect.init(value:)` and `Effect.init(error:)` no longer make sense. We will be retiring the latter some time in the future, so let's also get a head start and rename the former to `Effect.send`. For now it will call `Effect.init(value:)` under the hood, but in the future we will want a non-Combine-driven way of running synchronous effects. * format fix * wip * fix * wip * wip (cherry picked from commit e294b24edb998a62bdb43878c5972b72370474ab) # Conflicts: # Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md # Tests/ComposableArchitectureTests/CompatibilityTests.swift # Tests/ComposableArchitectureTests/ViewStoreTests.swift
1 parent 7d33b2d commit 084c6bd

File tree

11 files changed

+96
-55
lines changed

11 files changed

+96
-55
lines changed

Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ final class LoginSwiftUITests: XCTestCase {
1212
initialState: Login.State(),
1313
reducer: Login(),
1414
observe: LoginView.ViewState.init,
15-
send: action: Login.Action.init
15+
send: Login.Action.init
1616
) {
1717
$0.authenticationClient.login = { _ in
1818
AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false)
@@ -45,7 +45,7 @@ final class LoginSwiftUITests: XCTestCase {
4545
initialState: Login.State(),
4646
reducer: Login(),
4747
observe: LoginView.ViewState.init,
48-
send: action: Login.Action.init
48+
send: Login.Action.init
4949
) {
5050
$0.authenticationClient.login = { _ in
5151
AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: true)
@@ -82,7 +82,7 @@ final class LoginSwiftUITests: XCTestCase {
8282
initialState: Login.State(),
8383
reducer: Login(),
8484
observe: LoginView.ViewState.init,
85-
send: action: Login.Action.init
85+
send: Login.Action.init
8686
) {
8787
$0.authenticationClient.login = { _ in
8888
throw AuthenticationError.invalidUserPassword

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ test-examples:
4343
for scheme in "CaseStudies (SwiftUI)" "CaseStudies (UIKit)" Integration Search SpeechRecognition TicTacToe Todos VoiceMemos; do \
4444
xcodebuild test \
4545
-scheme "$$scheme" \
46-
-destination platform="$(PLATFORM_IOS)"; \
46+
-destination platform="$(PLATFORM_IOS)" || exit 1; \
4747
done
4848

4949
benchmark:

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,15 @@ struct Feature: ReducerProtocol {
236236
switch action {
237237
case .buttonTapped:
238238
state.count += 1
239-
return EffectTask(value: .sharedComputation)
239+
return .send(.sharedComputation)
240240

241241
case .toggleChanged:
242242
state.isEnabled.toggle()
243-
return EffectTask(value: .sharedComputation)
243+
return .send(.sharedComputation)
244244

245245
case let .textFieldChanged(text):
246246
state.description = text
247-
return EffectTask(value: .sharedComputation)
247+
return .send(.sharedComputation)
248248

249249
case .sharedComputation:
250250
// Some shared work to compute something.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
- ``EffectProducer/task(priority:operation:catch:file:fileID:line:)``
99
- ``EffectProducer/run(priority:operation:catch:file:fileID:line:)``
1010
- ``EffectProducer/fireAndForget(priority:_:)``
11+
- ``EffectProducer/send(_:)``
1112
- ``TaskResult``
1213

1314
### Cancellation
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# ``ComposableArchitecture/EffectPublisher/send(_:)``
2+
3+
## Topics
4+
5+
### Animating actions
6+
7+
- ``EffectPublisher/send(_:animation:)``

Sources/ComposableArchitecture/Effect.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,34 @@ extension EffectProducer where Failure == Never {
326326
) -> Self {
327327
Self.run(priority: priority) { _ in try? await work() }
328328
}
329+
330+
/// Initializes an effect that immediately emits the action passed in.
331+
///
332+
/// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
333+
/// > child-parent communication, where a child may want to emit a "delegate" action for a parent
334+
/// > to listen to.
335+
/// >
336+
/// > For more information, see <doc:Performance#Sharing-logic-with-actions>.
337+
///
338+
/// - Parameter action: The action that is immediately emitted by the effect.
339+
public static func send(_ action: Action) -> Self {
340+
Self(value: action)
341+
}
342+
343+
/// Initializes an effect that immediately emits the action passed in.
344+
///
345+
/// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
346+
/// > child-parent communication, where a child may want to emit a "delegate" action for a parent
347+
/// > to listen to.
348+
/// >
349+
/// > For more information, see <doc:Performance#Sharing-logic-with-actions>.
350+
///
351+
/// - Parameters:
352+
/// - action: The action that is immediately emitted by the effect.
353+
/// - animation: An animation.
354+
public static func send(_ action: Action, animation: Animation? = nil) -> Self {
355+
Self(value: action).animation(animation)
356+
}
329357
}
330358

331359
/// A type that can send actions back into the system when used from

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,7 +1889,8 @@ extension TestStore {
18891889
@available(
18901890
*,
18911891
deprecated,
1892-
message: """
1892+
message:
1893+
"""
18931894
Use 'TestStore.init(initialState:reducer:observe:send:)' to scope a test store's state and actions.
18941895
"""
18951896
)
@@ -1919,7 +1920,8 @@ extension TestStore {
19191920
@available(
19201921
*,
19211922
deprecated,
1922-
message: """
1923+
message:
1924+
"""
19231925
Use 'TestStore.init(initialState:reducer:observe:)' to scope a test store's state.
19241926
"""
19251927
)

Tests/ComposableArchitectureTests/CompatibilityTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ final class CompatibilityTests: XCTestCase {
4646
.cancellable(id: cancelID)
4747

4848
case .kickOffAction:
49-
return EffectTask(value: .actionSender(OnDeinit { observer.send(value: .stop) }))
49+
return .send(.actionSender(OnDeinit { observer.send(value: .stop) }))
5050

5151
case .actionSender:
5252
return .none

Tests/ComposableArchitectureTests/ReducerTests.swift

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -21,56 +21,58 @@ final class ReducerTests: XCTestCase {
2121
#if swift(>=5.7) && (canImport(RegexBuilder) || !os(macOS) && !targetEnvironment(macCatalyst))
2222
func testCombine_EffectsAreMerged() async throws {
2323
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) {
24-
enum Action: Equatable {
25-
case increment
26-
}
24+
try await _withMainSerialExecutor {
25+
enum Action: Equatable {
26+
case increment
27+
}
2728

28-
struct Delayed: ReducerProtocol {
29-
typealias State = Int
29+
struct Delayed: ReducerProtocol {
30+
typealias State = Int
3031

31-
@Dependency(\.continuousClock) var clock
32+
@Dependency(\.continuousClock) var clock
3233

33-
let delay: Duration
34-
let setValue: @Sendable () async -> Void
34+
let delay: Duration
35+
let setValue: @Sendable () async -> Void
3536

36-
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
37-
state += 1
38-
return .fireAndForget {
39-
try await self.clock.sleep(for: self.delay)
40-
await self.setValue()
37+
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
38+
state += 1
39+
return .fireAndForget {
40+
try await self.clock.sleep(for: self.delay)
41+
await self.setValue()
42+
}
4143
}
4244
}
43-
}
4445

45-
var fastValue: Int? = nil
46-
var slowValue: Int? = nil
46+
var fastValue: Int? = nil
47+
var slowValue: Int? = nil
4748

48-
let clock = TestClock()
49+
let clock = TestClock()
4950

50-
let store = TestStore(
51-
initialState: 0,
52-
reducer: CombineReducers {
53-
Delayed(delay: .seconds(1), setValue: { @MainActor in fastValue = 42 })
54-
Delayed(delay: .seconds(2), setValue: { @MainActor in slowValue = 1729 })
51+
let store = TestStore(
52+
initialState: 0,
53+
reducer: CombineReducers {
54+
Delayed(delay: .seconds(1), setValue: { @MainActor in fastValue = 42 })
55+
Delayed(delay: .seconds(2), setValue: { @MainActor in slowValue = 1729 })
56+
}
57+
) {
58+
$0.continuousClock = clock
5559
}
56-
) {
57-
$0.continuousClock = clock
58-
}
5960

60-
await store.send(.increment) {
61-
$0 = 2
61+
await store.send(.increment) {
62+
$0 = 2
63+
}
64+
// Waiting a second causes the fast effect to fire.
65+
await clock.advance(by: .seconds(1))
66+
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 3)
67+
XCTAssertEqual(fastValue, 42)
68+
XCTAssertEqual(slowValue, nil)
69+
// Waiting one more second causes the slow effect to fire. This proves that the effects
70+
// are merged together, as opposed to concatenated.
71+
await clock.advance(by: .seconds(1))
72+
await store.finish()
73+
XCTAssertEqual(fastValue, 42)
74+
XCTAssertEqual(slowValue, 1729)
6275
}
63-
// Waiting a second causes the fast effect to fire.
64-
await clock.advance(by: .seconds(1))
65-
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 3)
66-
XCTAssertEqual(fastValue, 42)
67-
XCTAssertEqual(slowValue, nil)
68-
// Waiting one more second causes the slow effect to fire. This proves that the effects
69-
// are merged together, as opposed to concatenated.
70-
await clock.advance(by: .seconds(1))
71-
await store.finish()
72-
XCTAssertEqual(fastValue, 42)
73-
XCTAssertEqual(slowValue, 1729)
7476
}
7577
}
7678
#endif

Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,10 @@
586586

587587
func testCasePathReceive_Exhaustive_NonEquatable() async {
588588
struct NonEquatable {}
589-
enum Action { case tap, response(NonEquatable) }
589+
enum Action {
590+
case tap
591+
case response(NonEquatable)
592+
}
590593

591594
let store = TestStore(
592595
initialState: 0,
@@ -606,7 +609,10 @@
606609

607610
func testPredicateReceive_Exhaustive_NonEquatable() async {
608611
struct NonEquatable {}
609-
enum Action { case tap, response(NonEquatable) }
612+
enum Action {
613+
case tap
614+
case response(NonEquatable)
615+
}
610616

611617
let store = TestStore(
612618
initialState: 0,

0 commit comments

Comments
 (0)