Skip to content

Commit e294b24

Browse files
authored
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
1 parent 6f33e07 commit e294b24

File tree

11 files changed

+115
-74
lines changed

11 files changed

+115
-74
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
- ``EffectPublisher/task(priority:operation:catch:file:fileID:line:)``
99
- ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)``
1010
- ``EffectPublisher/fireAndForget(priority:_:)``
11+
- ``EffectPublisher/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
@@ -323,6 +323,34 @@ extension EffectPublisher where Failure == Never {
323323
) -> Self {
324324
Self.run(priority: priority) { _ in try? await work() }
325325
}
326+
327+
/// Initializes an effect that immediately emits the action passed in.
328+
///
329+
/// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
330+
/// > child-parent communication, where a child may want to emit a "delegate" action for a parent
331+
/// > to listen to.
332+
/// >
333+
/// > For more information, see <doc:Performance#Sharing-logic-with-actions>.
334+
///
335+
/// - Parameter action: The action that is immediately emitted by the effect.
336+
public static func send(_ action: Action) -> Self {
337+
Self(value: action)
338+
}
339+
340+
/// Initializes an effect that immediately emits the action passed in.
341+
///
342+
/// > Note: We do not recommend using `Effect.send` to share logic. Instead, limit usage to
343+
/// > child-parent communication, where a child may want to emit a "delegate" action for a parent
344+
/// > to listen to.
345+
/// >
346+
/// > For more information, see <doc:Performance#Sharing-logic-with-actions>.
347+
///
348+
/// - Parameters:
349+
/// - action: The action that is immediately emitted by the effect.
350+
/// - animation: An animation.
351+
public static func send(_ action: Action, animation: Animation? = nil) -> Self {
352+
Self(value: action).animation(animation)
353+
}
326354
}
327355

328356
/// 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
@@ -1885,7 +1885,8 @@ extension TestStore {
18851885
@available(
18861886
*,
18871887
deprecated,
1888-
message: """
1888+
message:
1889+
"""
18891890
Use 'TestStore.init(initialState:reducer:observe:send:)' to scope a test store's state and actions.
18901891
"""
18911892
)
@@ -1915,7 +1916,8 @@ extension TestStore {
19151916
@available(
19161917
*,
19171918
deprecated,
1918-
message: """
1919+
message:
1920+
"""
19191921
Use 'TestStore.init(initialState:reducer:observe:)' to scope a test store's state.
19201922
"""
19211923
)

Tests/ComposableArchitectureTests/CompatibilityTests.swift

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

4949
case .kickOffAction:
50-
return EffectTask(value: .actionSender(OnDeinit { passThroughSubject.send(.stop) }))
50+
return .send(.actionSender(OnDeinit { passThroughSubject.send(.stop) }))
5151

5252
case .actionSender:
5353
return .none

Tests/ComposableArchitectureTests/ReducerTests.swift

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

29-
struct Delayed: ReducerProtocol {
30-
typealias State = Int
30+
struct Delayed: ReducerProtocol {
31+
typealias State = Int
3132

32-
@Dependency(\.continuousClock) var clock
33+
@Dependency(\.continuousClock) var clock
3334

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

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()
38+
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
39+
state += 1
40+
return .fireAndForget {
41+
try await self.clock.sleep(for: self.delay)
42+
await self.setValue()
43+
}
4244
}
4345
}
44-
}
4546

46-
var fastValue: Int? = nil
47-
var slowValue: Int? = nil
47+
var fastValue: Int? = nil
48+
var slowValue: Int? = nil
4849

49-
let clock = TestClock()
50+
let clock = TestClock()
5051

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 })
52+
let store = TestStore(
53+
initialState: 0,
54+
reducer: CombineReducers {
55+
Delayed(delay: .seconds(1), setValue: { @MainActor in fastValue = 42 })
56+
Delayed(delay: .seconds(2), setValue: { @MainActor in slowValue = 1729 })
57+
}
58+
) {
59+
$0.continuousClock = clock
5660
}
57-
) {
58-
$0.continuousClock = clock
59-
}
6061

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

Tests/ComposableArchitectureTests/TestStoreNonExhaustiveTests.swift

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

585585
func testCasePathReceive_Exhaustive_NonEquatable() async {
586586
struct NonEquatable {}
587-
enum Action { case tap, response(NonEquatable) }
587+
enum Action {
588+
case tap
589+
case response(NonEquatable)
590+
}
588591

589592
let store = TestStore(
590593
initialState: 0,
@@ -604,7 +607,10 @@
604607

605608
func testPredicateReceive_Exhaustive_NonEquatable() async {
606609
struct NonEquatable {}
607-
enum Action { case tap, response(NonEquatable) }
610+
enum Action {
611+
case tap
612+
case response(NonEquatable)
613+
}
608614

609615
let store = TestStore(
610616
initialState: 0,

Tests/ComposableArchitectureTests/ViewStoreTests.swift

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -167,33 +167,28 @@ final class ViewStoreTests: XCTestCase {
167167
XCTAssertEqual(results, Array(repeating: [0, 1, 2], count: 10).flatMap { $0 })
168168
}
169169

170-
func testSendWhile() {
171-
let expectation = self.expectation(description: "await")
172-
Task {
173-
enum Action {
174-
case response
175-
case tapped
176-
}
177-
let reducer = Reduce<Bool, Action> { state, action in
178-
switch action {
179-
case .response:
180-
state = false
181-
return .none
182-
case .tapped:
183-
state = true
184-
return .task { .response }
185-
}
170+
func testSendWhile() async {
171+
enum Action {
172+
case response
173+
case tapped
174+
}
175+
let reducer = Reduce<Bool, Action> { state, action in
176+
switch action {
177+
case .response:
178+
state = false
179+
return .none
180+
case .tapped:
181+
state = true
182+
return .task { .response }
186183
}
184+
}
187185

188-
let store = Store(initialState: false, reducer: reducer)
189-
let viewStore = ViewStore(store, observe: { $0 })
186+
let store = Store(initialState: false, reducer: reducer)
187+
let viewStore = ViewStore(store, observe: { $0 })
190188

191-
XCTAssertEqual(viewStore.state, false)
192-
await viewStore.send(.tapped, while: { $0 })
193-
XCTAssertEqual(viewStore.state, false)
194-
expectation.fulfill()
195-
}
196-
self.wait(for: [expectation], timeout: 1)
189+
XCTAssertEqual(viewStore.state, false)
190+
await viewStore.send(.tapped, while: { $0 })
191+
XCTAssertEqual(viewStore.state, false)
197192
}
198193

199194
func testSuspend() {

0 commit comments

Comments
 (0)