Skip to content

Commit c9cb61f

Browse files
authored
Merge pull request #42 from CombineCommunity/feature/use-replay-subject
Feature/use replay subject
2 parents f2d4483 + a774dfa commit c9cb61f

34 files changed

+275
-186
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
**v0.4.0 - Bane**:
2+
3+
- replace CurrentValueSubject by a ReplaySubject in System.stream
4+
15
**v0.3.0 - Tyranus**:
26

37
- Feedback: introduce the "on:" keyword to explicitly declare the type of state that concerns the side effect

Examples/Examples.xcodeproj/project.pbxproj

+10-22
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
1A7057E7266167540070FD5D /* Feedbacks in Frameworks */ = {isa = PBXBuildFile; productRef = 1A7057E6266167540070FD5D /* Feedbacks */; };
11+
1A7057E9266167950070FD5D /* FeedbacksTest in Frameworks */ = {isa = PBXBuildFile; productRef = 1A7057E8266167950070FD5D /* FeedbacksTest */; };
1012
741C474025EC5CDB00F1231B /* Counter+TransitionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 741C473F25EC5CDB00F1231B /* Counter+TransitionsTests.swift */; };
11-
741C475C25EC5DE100F1231B /* Feedbacks in Frameworks */ = {isa = PBXBuildFile; productRef = 741C475B25EC5DE100F1231B /* Feedbacks */; };
12-
741C476625EC5E4C00F1231B /* FeedbacksTest in Frameworks */ = {isa = PBXBuildFile; productRef = 741C476525EC5E4C00F1231B /* FeedbacksTest */; };
1313
742FEE2425B388DA00575CB2 /* GifList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742FEE2325B388DA00575CB2 /* GifList.swift */; };
1414
742FEE2825B38B1A00575CB2 /* GifList+States.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742FEE2725B38B1A00575CB2 /* GifList+States.swift */; };
1515
742FEE2E25B38EEE00575CB2 /* GifOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 742FEE2D25B38EEE00575CB2 /* GifOverview.swift */; };
@@ -66,6 +66,7 @@
6666
/* End PBXContainerItemProxy section */
6767

6868
/* Begin PBXFileReference section */
69+
1A5E6478266166CA00F576A9 /* Feedbacks */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Feedbacks; path = ..; sourceTree = "<group>"; };
6970
741C473D25EC5CDB00F1231B /* ExamplesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ExamplesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
7071
741C473F25EC5CDB00F1231B /* Counter+TransitionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Counter+TransitionsTests.swift"; sourceTree = "<group>"; };
7172
741C474125EC5CDB00F1231B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -121,15 +122,15 @@
121122
isa = PBXFrameworksBuildPhase;
122123
buildActionMask = 2147483647;
123124
files = (
124-
741C476625EC5E4C00F1231B /* FeedbacksTest in Frameworks */,
125+
1A7057E9266167950070FD5D /* FeedbacksTest in Frameworks */,
125126
);
126127
runOnlyForDeploymentPostprocessing = 0;
127128
};
128129
7471829D25AE7B0B0098E83E /* Frameworks */ = {
129130
isa = PBXFrameworksBuildPhase;
130131
buildActionMask = 2147483647;
131132
files = (
132-
741C475C25EC5DE100F1231B /* Feedbacks in Frameworks */,
133+
1A7057E7266167540070FD5D /* Feedbacks in Frameworks */,
133134
);
134135
runOnlyForDeploymentPostprocessing = 0;
135136
};
@@ -297,6 +298,7 @@
297298
7471829725AE7B0B0098E83E = {
298299
isa = PBXGroup;
299300
children = (
301+
1A5E6478266166CA00F576A9 /* Feedbacks */,
300302
747182A225AE7B0B0098E83E /* Examples */,
301303
741C473E25EC5CDB00F1231B /* ExamplesTests */,
302304
747182A125AE7B0B0098E83E /* Products */,
@@ -400,7 +402,7 @@
400402
);
401403
name = ExamplesTests;
402404
packageProductDependencies = (
403-
741C476525EC5E4C00F1231B /* FeedbacksTest */,
405+
1A7057E8266167950070FD5D /* FeedbacksTest */,
404406
);
405407
productName = ExamplesTests;
406408
productReference = 741C473D25EC5CDB00F1231B /* ExamplesTests.xctest */;
@@ -420,7 +422,7 @@
420422
);
421423
name = Examples;
422424
packageProductDependencies = (
423-
741C475B25EC5DE100F1231B /* Feedbacks */,
425+
1A7057E6266167540070FD5D /* Feedbacks */,
424426
);
425427
productName = Examples;
426428
productReference = 747182A025AE7B0B0098E83E /* Examples.app */;
@@ -454,7 +456,6 @@
454456
);
455457
mainGroup = 7471829725AE7B0B0098E83E;
456458
packageReferences = (
457-
741C475A25EC5DE100F1231B /* XCRemoteSwiftPackageReference "Feedbacks" */,
458459
);
459460
productRefGroup = 747182A125AE7B0B0098E83E /* Products */;
460461
projectDirPath = "";
@@ -789,26 +790,13 @@
789790
};
790791
/* End XCConfigurationList section */
791792

792-
/* Begin XCRemoteSwiftPackageReference section */
793-
741C475A25EC5DE100F1231B /* XCRemoteSwiftPackageReference "Feedbacks" */ = {
794-
isa = XCRemoteSwiftPackageReference;
795-
repositoryURL = "[email protected]:CombineCommunity/Feedbacks.git";
796-
requirement = {
797-
kind = exactVersion;
798-
version = 0.3.0;
799-
};
800-
};
801-
/* End XCRemoteSwiftPackageReference section */
802-
803793
/* Begin XCSwiftPackageProductDependency section */
804-
741C475B25EC5DE100F1231B /* Feedbacks */ = {
794+
1A7057E6266167540070FD5D /* Feedbacks */ = {
805795
isa = XCSwiftPackageProductDependency;
806-
package = 741C475A25EC5DE100F1231B /* XCRemoteSwiftPackageReference "Feedbacks" */;
807796
productName = Feedbacks;
808797
};
809-
741C476525EC5E4C00F1231B /* FeedbacksTest */ = {
798+
1A7057E8266167950070FD5D /* FeedbacksTest */ = {
810799
isa = XCSwiftPackageProductDependency;
811-
package = 741C475A25EC5DE100F1231B /* XCRemoteSwiftPackageReference "Feedbacks" */;
812800
productName = FeedbacksTest;
813801
};
814802
/* End XCSwiftPackageProductDependency section */

Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+13-4
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
"object": {
33
"pins": [
44
{
5-
"package": "Feedbacks",
6-
"repositoryURL": "git@github.com:CombineCommunity/Feedbacks.git",
5+
"package": "combine-schedulers",
6+
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers.git",
77
"state": {
88
"branch": null,
9-
"revision": "9d8ba9b4ada327b1f88401b822454770a6b65263",
10-
"version": "0.3.0"
9+
"revision": "ff42ec9061d864de7982162011321d3df5080c10",
10+
"version": "0.1.2"
11+
}
12+
},
13+
{
14+
"package": "CombineExt",
15+
"repositoryURL": "https://github.com/CombineCommunity/CombineExt.git",
16+
"state": {
17+
"branch": null,
18+
"revision": "5b8a0c0f178527f9204200505c5fefa6847e528f",
19+
"version": "1.3.0"
1120
}
1221
}
1322
]

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
}

Package.resolved

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
"revision": "ff42ec9061d864de7982162011321d3df5080c10",
1010
"version": "0.1.2"
1111
}
12+
},
13+
{
14+
"package": "CombineExt",
15+
"repositoryURL": "https://github.com/CombineCommunity/CombineExt.git",
16+
"state": {
17+
"branch": null,
18+
"revision": "5b8a0c0f178527f9204200505c5fefa6847e528f",
19+
"version": "1.3.0"
20+
}
1221
}
1322
]
1423
},

Package.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ let package = Package(
2323
dependencies: [
2424
// Dependencies declare other packages that this package depends on.
2525
.package(url: "https://github.com/pointfreeco/combine-schedulers.git", .exact(Version("0.1.2"))),
26+
.package(url: "https://github.com/CombineCommunity/CombineExt.git", .exact(Version("1.3.0")))
2627
],
2728
targets: [
2829
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2930
// Targets can depend on other targets in this package, and on products in packages this package depends on.
3031
.target(
3132
name: "Feedbacks",
32-
dependencies: [],
33+
dependencies: [.product(name: "CombineExt", package: "CombineExt")],
3334
path: "Sources/Feedbacks"),
3435
.testTarget(
3536
name: "FeedbacksTests",

README.md

+51-5
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ In our example, one feedback takes care of increasing the volume and the other i
9696

9797
## Scheduling
9898

99-
Threading is very important to make a nice responsive application. A Scheduler is the Combine way of handling threading by switching portions of reactive streams on dispatch queues, or operation queues or RunLoops.
99+
Threading is very important to make a nice responsive application. A Scheduler is the Combine way of handling threading by switching portions of reactive streams on dispatch queues, operation queues or RunLoops.
100100

101-
The declarative syntax of Feedbacks allows to alter the behavior of side effects by simply applying modifiers (like you would do with SwiftUI to change the frame for instance). Modifying the scheduling of a side effect is as simple as calling the `.execute(on:)` modifier.
101+
The declarative syntax of Feedbacks allows to alter the behavior of a System by simply applying modifiers (like you would do with SwiftUI to change the frame for instance). Modifying the scheduling of a side effect is as simple as calling the `.execute(on:)` modifier.
102102

103103
```swift
104104
Feedbacks {
@@ -128,6 +128,52 @@ Feedbacks {
128128

129129
Both side effects will be executed on the background queue.
130130

131+
It is also applicable to the transitions:
132+
133+
```swift
134+
Transitions {
135+
From(VolumeState.self) { state in
136+
On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))
137+
On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))
138+
}
139+
}.execute(on: DispatchQueue(label: "A background queue"))
140+
```
141+
142+
or to the whole system:
143+
144+
```swift
145+
System {
146+
InitialState {
147+
VolumeState(value: 10)
148+
}
149+
150+
Feedbacks {
151+
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
152+
if state.value >= targetedVolume {
153+
return Empty().eraseToAnyPublisher()
154+
}
155+
156+
return Just(IncreaseEvent()).eraseToAnyPublisher()
157+
}
158+
159+
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
160+
if state.value <= targetedVolume {
161+
return Empty().eraseToAnyPublisher()
162+
}
163+
164+
return Just(DecreaseEvent()).eraseToAnyPublisher()
165+
}
166+
}
167+
168+
Transitions {
169+
From(VolumeState.self) { state in
170+
On(IncreaseEvent.self, transitionTo: VolumeState(value: state.value + 1))
171+
On(DecreaseEvent.self, transitionTo: VolumeState(value: state.value - 1))
172+
}
173+
}
174+
}.execute(on: DispatchQueue(label: "A background queue"))
175+
```
176+
131177
## Lifecycle
132178

133179
There are typical cases where a side effect consist of an asynchronous operation (like a network call). What happens if the very same side effect is called repeatedly, not waiting for the previous ones to end? Are the operations stacked? Are they cancelled when a new one is performed?
@@ -196,9 +242,9 @@ Here is a list of the supported modifiers:
196242
| Modifier | Action | Can be applied to |
197243
| -------------- | -------------- | -------------- |
198244
| `.disable(disabled:)`| The target won't be executed as long as the `disabled` condition is true | <ul align="left"><li>Transition</li><li>Transitions</li><li>Feedback</li></ul> |
199-
| `.execute(on:)`| The target will be executed on the scheduler | <ul align="left"><li>Feedbacks</li><li>Feedback</li></ul> |
200-
| `.onStateReceived(perform:)`| Execute the `perform` closure each time a new state is given as an input | <ul align="left"><li>Feedbacks</li><li>Feedback</li></ul> |
201-
| `.onEventEmitted(perform:)`| Execute the `perform` closure each time a new event is emitted | <ul align="left"><li>Feedbacks</li><li>Feedback</li></ul> |
245+
| `.execute(on:)`| The target will be executed on the scheduler | <ul align="left"><li>Transitions</li><li>Feedback</li><li>Feedbacks</li><li>System</li></ul> |
246+
| `.onStateReceived(perform:)`| Execute the `perform` closure each time a new state is given as an input | <ul align="left"><li>Feedback</li><li>Feedbacks</li></ul> |
247+
| `.onEventEmitted(perform:)`| Execute the `perform` closure each time a new event is emitted | <ul align="left"><li>Feedback</li><li>Feedbacks</li></ul> |
202248
| `.attach(to:)`| Refer to the "How to make systems communicate" section | <ul align="left"><li>System</li><li>UISystem</li></ul> |
203249
| `.uiSystem(viewStateFactory:)`| Refer to the "Using Feedbacks with SwiftUI and UIKit" section | <ul align="left"><li>System</li></ul> |
204250

Sources/Feedbacks/System/System.swift

+18-26
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//
77

88
import Combine
9+
import CombineExt
910
import Dispatch
1011
import Foundation
1112

@@ -17,37 +18,26 @@ import Foundation
1718
public class System {
1819
let initialState: InitialState
1920
var feedbacks: Feedbacks
20-
public let transitions: Transitions
21-
var scheduledStream: (AnyPublisher<Event, Never>) -> AnyPublisher<Event, Never>
21+
public private(set) var transitions: Transitions
2222

2323
private var subscriptions = [AnyCancellable]()
2424

25-
static let defaultQueue = DispatchQueue(label: "Feedbacks.System.\(UUID().uuidString)")
26-
2725
/// Builds a System based on its three components: an initial state, some feedbacks, a state machine
2826
/// By default, the System will be executed an a serial background queue. This can be altered thanks to the `.execute(on:)` modifier.
2927
/// - Parameter components: the three components of the System
3028
public convenience init(@SystemBuilder _ components: () -> (InitialState, Feedbacks, Transitions)) {
3129
let (initialState, feedbacks, transitions) = System.decode(builder: components)
3230
self.init(initialState: initialState,
3331
feedbacks: feedbacks,
34-
transitions: transitions,
35-
scheduledStream: { (events: AnyPublisher<Event, Never>) in
36-
events
37-
.subscribe(on: System.defaultQueue)
38-
.receive(on: System.defaultQueue)
39-
.eraseToAnyPublisher()
40-
})
32+
transitions: transitions)
4133
}
4234

4335
init(initialState: InitialState,
4436
feedbacks: Feedbacks,
45-
transitions: Transitions,
46-
scheduledStream: @escaping (AnyPublisher<Event, Never>) -> AnyPublisher<Event, Never>) {
37+
transitions: Transitions) {
4738
self.initialState = initialState
4839
self.feedbacks = feedbacks
4940
self.transitions = transitions
50-
self.scheduledStream = scheduledStream
5141
}
5242

5343
static func decode(builder system: () -> (InitialState, Feedbacks, Transitions)) -> (InitialState, Feedbacks, Transitions) {
@@ -61,16 +51,15 @@ public extension System {
6151
/// Once this stream has been subscribed to, the initial state is given as an input to the feedbacks.
6252
/// Then the feedbacks can publish event that will trigger some transitions, generating a new state, and so on and so forth.
6353
var stream: AnyPublisher<State, Never> {
64-
Deferred<AnyPublisher<State, Never>> { [initialState, feedbacks, transitions, scheduledStream] in
65-
let currentState = CurrentValueSubject<State, Never>(initialState.value)
54+
Deferred<AnyPublisher<State, Never>> { [initialState, feedbacks, transitions] in
55+
let currentState = ReplaySubject<State, Never>(bufferSize: 1)
6656

6757
// merging all the effects into one event stream
6858
let stateInputStream = currentState.eraseToAnyPublisher()
6959
let eventStream = feedbacks.eventStream(stateInputStream)
70-
let scheduledEventStream = scheduledStream(eventStream)
7160

72-
return scheduledEventStream
73-
.scan(initialState.value, transitions.reducer)
61+
return transitions.scheduledReducer(initialState.value, eventStream)
62+
.prepend(initialState.value)
7463
.handleEvents(receiveOutput: currentState.send)
7564
.eraseToAnyPublisher()
7665
}.eraseToAnyPublisher()
@@ -83,6 +72,14 @@ public extension System {
8372
self.stream.sink(receiveValue: { _ in }).store(in: &self.subscriptions)
8473
return self
8574
}
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+
}
8683
}
8784

8885
// MARK: modifiers
@@ -93,13 +90,8 @@ public extension System {
9390
/// - Parameter scheduler: the scheduler on which to execute the System
9491
/// - Returns: The newly scheduled System
9592
func execute<SchedulerType: Scheduler>(on scheduler: SchedulerType) -> Self {
96-
self.scheduledStream = { events in
97-
events
98-
.subscribe(on: scheduler)
99-
.receive(on: scheduler)
100-
.eraseToAnyPublisher()
101-
}
102-
93+
self.feedbacks = self.feedbacks.execute(on: scheduler)
94+
self.transitions = self.transitions.execute(on: scheduler)
10395
return self
10496
}
10597

0 commit comments

Comments
 (0)