Skip to content

Commit 29a13dd

Browse files
committed
system: refactor propagate execute(on:)
1 parent 9cd0533 commit 29a13dd

File tree

16 files changed

+183
-153
lines changed

16 files changed

+183
-153
lines changed

Examples/Examples/CounterApp/System/CounterApp+System.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Thibault Wittemberg on 2021-01-12.
66
//
77

8+
import Dispatch
89
import Feedbacks
910

1011
// define a namespace for this app's system
@@ -55,5 +56,5 @@ extension CounterApp.System {
5556
On(CounterApp.Events.Decrease.self, transitionTo: CounterApp.States.Decreasing(counter: state.counter.decrease(), isPaused: false))
5657
}
5758
}
58-
}
59+
}.execute(on: DispatchQueue(label: "Counter Queue"))
5960
}

Examples/Examples/GiphyApp/Features/GifDetail/System/GifDetail+System.swift

+1-4
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,7 @@ extension GifDetail.System {
3232

3333
Feedbacks {
3434
Feedback(on: GifDetail.States.Loading.self, strategy: .cancelOnNewState, perform: loadSideEffect)
35-
.execute(on: DispatchQueue(label: "Load Gif Queue"))
36-
3735
Feedback(on: GifDetail.States.TogglingFavorite.self, strategy: .cancelOnNewState, perform: toggleFavoriteSideEffect)
38-
.execute(on: DispatchQueue(label: "Toggle Favorite Queue"))
3936
}
4037
.onStateReceived {
4138
print("GifDetail: New state has been received: \($0)")
@@ -64,6 +61,6 @@ extension GifDetail.System {
6461
On(GifDetail.Events.LoadingHasFailed.self, transitionTo: GifDetail.States.Failed())
6562
}
6663
}
67-
}
64+
}.execute(on: DispatchQueue(label: "Load Gif Queue"))
6865
}
6966
}

Examples/Examples/GiphyApp/Features/GifList/System/GifList+System.swift

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ extension GifList.System {
2626

2727
Feedbacks {
2828
Feedback(on: GifList.States.Loading.self , strategy: .cancelOnNewState, perform: loadSideEffect)
29-
.execute(on: DispatchQueue(label: "Load Gifs Queue"))
3029
}
3130
.onStateReceived {
3231
print("GifList: New state has been received: \($0)")
@@ -64,6 +63,6 @@ extension GifList.System {
6463
On(GifList.Events.Refresh.self, transitionTo: GifList.States.Loading())
6564
}
6665
}
67-
}
66+
}.execute(on: DispatchQueue(label: "Load Gifs Queue"))
6867
}
6968
}

Sources/Feedbacks/System/System.swift

+15-25
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,26 @@ import Foundation
1818
public class System {
1919
let initialState: InitialState
2020
var feedbacks: Feedbacks
21-
public let transitions: Transitions
22-
var scheduledStream: (AnyPublisher<Event, Never>) -> AnyPublisher<Event, Never>
21+
public private(set) var transitions: Transitions
2322

2423
private var subscriptions = [AnyCancellable]()
2524

26-
static let defaultQueue = DispatchQueue(label: "Feedbacks.System.\(UUID().uuidString)")
27-
2825
/// Builds a System based on its three components: an initial state, some feedbacks, a state machine
2926
/// By default, the System will be executed an a serial background queue. This can be altered thanks to the `.execute(on:)` modifier.
3027
/// - Parameter components: the three components of the System
3128
public convenience init(@SystemBuilder _ components: () -> (InitialState, Feedbacks, Transitions)) {
3229
let (initialState, feedbacks, transitions) = System.decode(builder: components)
3330
self.init(initialState: initialState,
3431
feedbacks: feedbacks,
35-
transitions: transitions,
36-
scheduledStream: { (events: AnyPublisher<Event, Never>) in
37-
events
38-
.subscribe(on: System.defaultQueue)
39-
.receive(on: System.defaultQueue)
40-
.eraseToAnyPublisher()
41-
})
32+
transitions: transitions)
4233
}
4334

4435
init(initialState: InitialState,
4536
feedbacks: Feedbacks,
46-
transitions: Transitions,
47-
scheduledStream: @escaping (AnyPublisher<Event, Never>) -> AnyPublisher<Event, Never>) {
37+
transitions: Transitions) {
4838
self.initialState = initialState
4939
self.feedbacks = feedbacks
5040
self.transitions = transitions
51-
self.scheduledStream = scheduledStream
5241
}
5342

5443
static func decode(builder system: () -> (InitialState, Feedbacks, Transitions)) -> (InitialState, Feedbacks, Transitions) {
@@ -62,16 +51,14 @@ public extension System {
6251
/// Once this stream has been subscribed to, the initial state is given as an input to the feedbacks.
6352
/// Then the feedbacks can publish event that will trigger some transitions, generating a new state, and so on and so forth.
6453
var stream: AnyPublisher<State, Never> {
65-
Deferred<AnyPublisher<State, Never>> { [initialState, feedbacks, transitions, scheduledStream] in
54+
Deferred<AnyPublisher<State, Never>> { [initialState, feedbacks, transitions] in
6655
let currentState = ReplaySubject<State, Never>(bufferSize: 1)
6756

6857
// merging all the effects into one event stream
6958
let stateInputStream = currentState.eraseToAnyPublisher()
7059
let eventStream = feedbacks.eventStream(stateInputStream)
71-
let scheduledEventStream = scheduledStream(eventStream)
7260

73-
return scheduledEventStream
74-
.scan(initialState.value, transitions.reducer)
61+
return transitions.scheduledReducer(initialState.value, eventStream)
7562
.prepend(initialState.value)
7663
.handleEvents(receiveOutput: currentState.send)
7764
.eraseToAnyPublisher()
@@ -85,6 +72,14 @@ public extension System {
8572
self.stream.sink(receiveValue: { _ in }).store(in: &self.subscriptions)
8673
return self
8774
}
75+
76+
/// Subscribes to the state stream and store the cancellable in the System.
77+
/// The subscription will be canceled once the System is deinit.
78+
@discardableResult
79+
func run<SchedulerType: Scheduler>(subscribeOn scheduler: SchedulerType) -> Self {
80+
self.stream.subscribe(on: scheduler).sink(receiveValue: { _ in }).store(in: &self.subscriptions)
81+
return self
82+
}
8883
}
8984

9085
// MARK: modifiers
@@ -95,13 +90,8 @@ public extension System {
9590
/// - Parameter scheduler: the scheduler on which to execute the System
9691
/// - Returns: The newly scheduled System
9792
func execute<SchedulerType: Scheduler>(on scheduler: SchedulerType) -> Self {
98-
self.scheduledStream = { events in
99-
events
100-
.subscribe(on: scheduler)
101-
.receive(on: scheduler)
102-
.eraseToAnyPublisher()
103-
}
104-
93+
self.feedbacks = self.feedbacks.execute(on: scheduler)
94+
self.transitions = self.transitions.execute(on: scheduler)
10595
return self
10696
}
10797

Sources/Feedbacks/System/UISystem.swift

+11-26
Original file line numberDiff line numberDiff line change
@@ -42,31 +42,22 @@ public class UISystem<PublishedState: State>: System, ObservableObject {
4242

4343
self.init(initialState: initialState,
4444
feedbacks: feedbacks,
45-
transitions: transitions,
46-
scheduledStream: { (events: AnyPublisher<Event, Never>) in
47-
events
48-
.subscribe(on: System.defaultQueue)
49-
.receive(on: System.defaultQueue)
50-
.eraseToAnyPublisher()
51-
})
45+
transitions: transitions)
5246
}
5347

5448
convenience init(system: System) where PublishedState == RawState {
5549
self.init(initialState: system.initialState,
5650
feedbacks: system.feedbacks,
57-
transitions: system.transitions,
58-
scheduledStream: system.scheduledStream)
51+
transitions: system.transitions)
5952
}
6053

6154
override init(initialState: InitialState,
6255
feedbacks: Feedbacks,
63-
transitions: Transitions,
64-
scheduledStream: @escaping (AnyPublisher<Event, Never>) -> AnyPublisher<Event, Never>) where PublishedState == RawState {
56+
transitions: Transitions) where PublishedState == RawState {
6557
self.state = RawState(state: initialState.value)
6658
super.init(initialState: initialState,
6759
feedbacks: feedbacks,
68-
transitions: transitions,
69-
scheduledStream: scheduledStream)
60+
transitions: transitions)
7061

7162
let stateFeedback = Self.makeStatePublishingFeedback(publishingFunction: { [weak self] in
7263
self?.state = $0
@@ -76,7 +67,9 @@ public class UISystem<PublishedState: State>: System, ObservableObject {
7667
events.eraseToAnyPublisher()
7768
}
7869

79-
self.feedbacks = self.feedbacks.add(feedback: stateFeedback).add(feedback: eventFeedback)
70+
self.feedbacks = self.feedbacks
71+
.add(feedback: stateFeedback)
72+
.add(feedback: eventFeedback)
8073
}
8174

8275
/// Creates a UISystem based on the 3 components of a System (initial state, feedbacks, state machine) and a View State factory function
@@ -85,7 +78,7 @@ public class UISystem<PublishedState: State>: System, ObservableObject {
8578
/// - system: the 3 components of the System
8679
public convenience init(
8780
viewStateFactory: @escaping (State) -> PublishedState,
88-
@SystemBuilder _ components: () -> (InitialState, Feedbacks, Transitions)
81+
@SystemBuilder _ components: () -> (InitialState, Feedbacks, Transitions)
8982
) where PublishedState: ViewState {
9083
self.init(viewStateFactory: viewStateFactory,
9184
on: DispatchQueue(label: "Feedbacks.UISystem.\(UUID().uuidString)"),
@@ -99,21 +92,15 @@ public class UISystem<PublishedState: State>: System, ObservableObject {
9992
/// - system: the 3 components of the System
10093
public convenience init<SchedulerType: Scheduler>(
10194
viewStateFactory: @escaping (State) -> PublishedState,
102-
on scheduler: SchedulerType,
103-
@SystemBuilder _ components: () -> (InitialState, Feedbacks, Transitions)
95+
on scheduler: SchedulerType,
96+
@SystemBuilder _ components: () -> (InitialState, Feedbacks, Transitions)
10497
) where PublishedState: ViewState {
10598
let (initialState, feedbacks, transitions) = System.decode(builder: components)
10699

107100
self.init(viewStateFactory: viewStateFactory,
108101
initialState: initialState,
109102
feedbacks: feedbacks,
110103
transitions: transitions,
111-
scheduledStream: { (events: AnyPublisher<Event, Never>) in
112-
events
113-
.subscribe(on: System.defaultQueue)
114-
.receive(on: System.defaultQueue)
115-
.eraseToAnyPublisher()
116-
},
117104
viewStateScheduler: scheduler)
118105
}
119106

@@ -131,7 +118,6 @@ public class UISystem<PublishedState: State>: System, ObservableObject {
131118
initialState: system.initialState,
132119
feedbacks: system.feedbacks,
133120
transitions: system.transitions,
134-
scheduledStream: system.scheduledStream,
135121
viewStateScheduler: scheduler)
136122
}
137123

@@ -140,14 +126,13 @@ public class UISystem<PublishedState: State>: System, ObservableObject {
140126
initialState: InitialState,
141127
feedbacks: Feedbacks,
142128
transitions: Transitions,
143-
scheduledStream: @escaping (AnyPublisher<Event, Never>) -> AnyPublisher<Event, Never>,
144129
viewStateScheduler: ViewStateSchedulerType
145130
) where PublishedState: ViewState {
146131
// since the initial view state is calculated asynchronously on the viewStateScheduler when the system is started
147132
// we set it to a initial undefined value
148133
self.state = PublishedState.undefined
149134

150-
super.init(initialState: initialState, feedbacks: feedbacks, transitions: transitions, scheduledStream: scheduledStream)
135+
super.init(initialState: initialState, feedbacks: feedbacks, transitions: transitions)
151136

152137
let stateFeedback = Self.makeStatePublishingFeedback(
153138
viewStateFactory: viewStateFactory,

Sources/Feedbacks/Transitions/Transitions.swift

+34-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
//
55
// Created by Thibault Wittemberg on 2020-12-23.
66
//
7+
import Combine
78

89
/// Represents a series of Transitions that drive a State Machine.
910
public struct Transitions {
1011
let transitions: [From]
1112

1213
/// the reducer computed from the state machine's transitions
1314
public let reducer: (State, Event) -> State
15+
public let scheduledReducer: (State, AnyPublisher<Event, Never>) -> AnyPublisher<State, Never>
1416

1517
/// - Parameter transitions: the individual transitions composing the state machine
1618
/// Transitions {
@@ -31,11 +33,29 @@ public struct Transitions {
3133

3234
init(transitions: [From]) {
3335
self.transitions = transitions
34-
let transitionsForStates = self.transitions.reduce(into: [AnyHashable: (State) -> [AnyHashable: (Event) -> State?]]()) { accumulator, from in
36+
self.reducer = Transitions.makeReducer(transitions: transitions)
37+
self.scheduledReducer = { [reducer] initialState, events in
38+
return events
39+
.scan(initialState, reducer)
40+
.eraseToAnyPublisher()
41+
}
42+
}
43+
44+
init(transitions: [From],
45+
reducer: @escaping (State, Event) -> State,
46+
scheduledReducer: @escaping (State, AnyPublisher<Event, Never>) -> AnyPublisher<State, Never>) {
47+
self.transitions = transitions
48+
self.reducer = reducer
49+
self.scheduledReducer = scheduledReducer
50+
}
51+
52+
static func makeReducer(transitions: [From]) -> (State, Event) -> State {
53+
let transitionsForStates = transitions.reduce(into: [AnyHashable: (State) -> [AnyHashable: (Event) -> State?]]()) { accumulator, from in
3554
let existingTranstionsForState = accumulator[from.id]
3655
accumulator[from.id] = { state in from.computeTransitionsForEvents(for: state, existingTranstionsForState: existingTranstionsForState) }
3756
}
38-
self.reducer = { state, event -> State in
57+
58+
return { state, event -> State in
3959
if
4060
let transitionsForState = transitionsForStates[state.instanceId],
4161
let transitionForEvent = transitionsForState(state)[event.instanceId],
@@ -69,4 +89,16 @@ public extension Transitions {
6989
func disable(_ disabled: @escaping () -> Bool) -> Self {
7090
Transitions(transitions: self.transitions.map { $0.disable(disabled) })
7191
}
92+
93+
/// Alter the scheduler on which the Transitions run. If no schedulers are
94+
/// set for the Transitions, then they will be executed on the current scheduler.
95+
/// - Parameter scheduler: the scheduler on which to execute the Transitions
96+
/// - Returns: The newly scheduled Transitions
97+
func execute<SchedulerType: Scheduler>(on scheduler: SchedulerType) -> Self {
98+
let newScheduledReducer: (State, AnyPublisher<Event, Never>) -> AnyPublisher<State, Never> = { [scheduledReducer] initialState, events in
99+
return scheduledReducer(initialState, events.receive(on: scheduler).eraseToAnyPublisher())
100+
}
101+
102+
return Transitions(transitions: self.transitions, reducer: self.reducer, scheduledReducer: newScheduledReducer)
103+
}
72104
}

Tests/FeedbacksTests/System/FeedbackTests.swift renamed to Tests/FeedbacksTests/Feedbacks/FeedbackTests.swift

+27
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,33 @@ extension FeedbackTests {
228228

229229
cancellable.cancel()
230230
}
231+
232+
func testExecute_execute_sideEffect_on_the_inner_scheduler() {
233+
let exp = expectation(description: "Feedback.execute(on:)")
234+
235+
let expectedQueue = UUID().uuidString
236+
var receivedQueue = ""
237+
238+
// Given: a side effect recording its execution queue
239+
let spySideEffect: (MockStateA) -> AnyPublisher<Event, Never> = { state in
240+
receivedQueue = DispatchQueue.currentLabel
241+
return Just(MockEventA(value: 1)).eraseToAnyPublisher()
242+
}
243+
244+
// When: making a Feedback of it, and executing it on the expected Queue
245+
let sut = Feedback(on: MockStateA.self, strategy: .continueOnNewState, perform: spySideEffect)
246+
.execute(on: DispatchQueue(label: expectedQueue))
247+
.execute(on: DispatchQueue(label: UUID().uuidString))
248+
249+
let cancellable = sut.sideEffect(Just(MockStateA(value: 1)).eraseToAnyPublisher()).sink{ _ in exp.fulfill() }
250+
251+
waitForExpectations(timeout: 0.5)
252+
253+
// Then: the side effect is executed on the inner scheduler
254+
XCTAssertEqual(receivedQueue, expectedQueue)
255+
256+
cancellable.cancel()
257+
}
231258
}
232259

233260
// MARK: tests for Feedback.disable(:)

0 commit comments

Comments
 (0)