Skip to content

Commit e948f9b

Browse files
authored
Merge pull request #36 from CombineCommunity/improve/feedback-dsl
Improve/feedback dsl
2 parents 73306ac + 2357451 commit e948f9b

File tree

13 files changed

+218
-104
lines changed

13 files changed

+218
-104
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
**v0.3.0 - Tyranus**:
2+
3+
- Feedback: introduce the "on:" keyword to explicitly declare the type of state that concerns the side effect
4+
15
**v0.2.0 - Vader**:
26

37
- UISystem: unify the UISystem concept for RawState and ViewState

Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,8 +626,8 @@
626626
isa = XCRemoteSwiftPackageReference;
627627
repositoryURL = "[email protected]:twittemb/Feedbacks.git";
628628
requirement = {
629-
kind = exactVersion;
630-
version = 0.2.0;
629+
kind = revision;
630+
revision = ceca7e90a065cbb67346d0234243f598500ddfc5;
631631
};
632632
};
633633
/* End XCRemoteSwiftPackageReference section */

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

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ extension CounterApp.System {
1919
}
2020

2121
Feedbacks {
22-
Feedback(strategy: .cancelOnNewState, sideEffect: CounterApp.SideEffects.decreaseEffect(state:))
23-
Feedback(strategy: .cancelOnNewState, sideEffect: CounterApp.SideEffects.increaseEffect(state:))
22+
Feedback(on: CounterApp.States.Decreasing.self,
23+
strategy: .cancelOnNewState,
24+
sideEffect: CounterApp.SideEffects.decreaseEffect(state:))
25+
26+
Feedback(on: CounterApp.States.Increasing.self,
27+
strategy: .cancelOnNewState,
28+
sideEffect: CounterApp.SideEffects.increaseEffect(state:))
2429
}
2530
.onStateReceived {
2631
print("Counter: New state has been received: \($0)")

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ extension GifDetail.System {
3131
}
3232

3333
Feedbacks {
34-
Feedback(strategy: .cancelOnNewState, sideEffect: loadSideEffect)
34+
Feedback(on: GifDetail.States.Loading.self, strategy: .cancelOnNewState, sideEffect: loadSideEffect)
3535
.execute(on: DispatchQueue(label: "Load Gif Queue"))
3636

37-
Feedback(strategy: .cancelOnNewState, sideEffect: toggleFavoriteSideEffect)
37+
Feedback(on: GifDetail.States.TogglingFavorite.self, strategy: .cancelOnNewState, sideEffect: toggleFavoriteSideEffect)
3838
.execute(on: DispatchQueue(label: "Toggle Favorite Queue"))
3939
}
4040
.onStateReceived {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ extension GifList.System {
2525
}
2626

2727
Feedbacks {
28-
Feedback(strategy: .cancelOnNewState, sideEffect: loadSideEffect)
28+
Feedback(on: GifList.States.Loading.self , strategy: .cancelOnNewState, sideEffect: loadSideEffect)
2929
.execute(on: DispatchQueue(label: "Load Gifs Queue"))
3030
}
3131
.onStateReceived {

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ let system = System {
3737
}
3838

3939
Feedbacks {
40-
Feedback(strategy: .continueOnNewState) { (state: VolumeState) -> AnyPublisher<Event, Never> in
40+
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
4141
if state.value >= targetedVolume {
4242
return Empty().eraseToAnyPublisher()
4343
}
4444

4545
return Just(IncreaseEvent()).eraseToAnyPublisher()
4646
}
4747

48-
Feedback(strategy: .continueOnNewState) { (state: VolumeState) -> AnyPublisher<Event, Never> in
48+
Feedback(on: VolumeState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
4949
if state.value <= targetedVolume {
5050
return Empty().eraseToAnyPublisher()
5151
}
@@ -105,7 +105,7 @@ The declarative syntax of Feedbacks allows to alter the behavior of side effects
105105

106106
```swift
107107
Feedbacks {
108-
Feedback(strategy: .continueOnNewState) { (state: LoadingState) -> AnyPublisher<Event, Never> in
108+
Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
109109
performLongRunningOperation()
110110
.map { FinishedLoadingEvent() }
111111
.eraseToAnyPublisher()
@@ -118,11 +118,11 @@ As in SwiftUI, modifiers can be applied to the container:
118118

119119
```swift
120120
Feedbacks {
121-
Feedback(strategy: .continueOnNewState) { (state: LoadingState) -> AnyPublisher<Event, Never> in
121+
Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
122122
...
123123
}
124124

125-
Feedback(strategy: .continueOnNewState) { (state: SelectedState) -> AnyPublisher<Event, Never> in
125+
Feedback(on: SelectedState.self, strategy: .continueOnNewState) { state -> AnyPublisher<Event, Never> in
126126
...
127127
}
128128
}
@@ -159,8 +159,8 @@ enum MySideEffects {
159159

160160
let myNetworkService = MyNetworkService()
161161
let myDatabaseService = MyDatabaseService()
162-
let mySideEffect = SideEffect.make(MySideEffects.load, arg1: myNetworkService, arg2: myDatabaseService)
163-
let feedback = Feedback(strategy: .cancelOnNewState, sideEffect: mySideEffect)
162+
let loadingEffect = SideEffect.make(MySideEffects.load, arg1: myNetworkService, arg2: myDatabaseService)
163+
let feedback = Feedback(on: LoadingState.self, strategy: .cancelOnNewState, sideEffect: loadingEffect)
164164
```
165165

166166
`SideEffect.make()` factories will transform functions with several parameters (up to 6 including the state) into functions with 1 parameter (the state), on the condition of the state being the last one.
@@ -238,17 +238,17 @@ Everytime the RefreshEvent is received, this transition will produce a LoadingSt
238238
A Feedback is built from a side effect. A side effect is a function that takes a state as a parameter. There are two ways to build a Feedback:
239239

240240
```swift
241-
Feedback(strategy: .continueOnNewState) { (state: State) in
241+
Feedback(on: AnyState.self, strategy: .continueOnNewState) { state in
242242
...
243243
.map { _ in MyEvent() }
244244
.eraseToAnyPublisher()
245245
}
246246
```
247247

248-
This feedback will execute the side effect for any type of state. It could be useful if you want to perform a side effect each time a new state is generated, regardless of the type of State.
248+
This feedback will execute the side effect whatever the type of state that is produced. It could be useful if you want to perform a side effect each time a new state is generated, regardless of the type of State.
249249

250250
```swift
251-
Feedback(strategy: .continueOnNewState) { (state: LoadingState) in
251+
Feedback(on: LoadingState.self, strategy: .continueOnNewState) { state in
252252
...
253253
.map { _ in MyEvent() }
254254
.eraseToAnyPublisher()

Sources/Feedbacks/System/Feedback.swift

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ public struct Feedback {
1818
case cancelOnNewState
1919
case continueOnNewState
2020

21+
func apply<StateType: State>(
22+
on sideEffect: @escaping (StateType) -> AnyPublisher<Event, Never>,
23+
willExecuteWithStrategy: @escaping (Feedback.Strategy) -> Void = { _ in }
24+
) -> (AnyPublisher<StateType, Never>) -> AnyPublisher<Event, Never> {
25+
return { states in
26+
willExecuteWithStrategy(self)
27+
switch self {
28+
case .cancelOnNewState:
29+
return states.map(sideEffect).switchToLatest().eraseToAnyPublisher()
30+
case .continueOnNewState:
31+
return states.flatMap(sideEffect).eraseToAnyPublisher()
32+
}
33+
}
34+
}
35+
2136
func apply(
2237
on sideEffect: @escaping (State) -> AnyPublisher<Event, Never>,
2338
willExecuteWithStrategy: @escaping (Feedback.Strategy) -> Void = { _ in }
@@ -36,37 +51,55 @@ public struct Feedback {
3651

3752
let sideEffect: (AnyPublisher<State, Never>) -> AnyPublisher<Event, Never>
3853

39-
/// Creates a Feedback based on a side effect to execute
40-
/// - Parameter sideEffect: the side effect to execute in the context of this feedback
41-
public init(sideEffect: @escaping (AnyPublisher<State, Never>) -> AnyPublisher<Event, Never>) {
54+
init(sideEffect: @escaping (AnyPublisher<State, Never>) -> AnyPublisher<Event, Never>) {
4255
self.sideEffect = sideEffect
4356
}
4457

58+
/// Creates a Feedback based on a side effect to execute
59+
/// - Parameters:
60+
/// - on: The type of state that should trigger the side effect (forced to AnyState)
61+
/// - sideEffect: the side effect to execute in the context of this feedback
62+
public init(on: AnyState.Type,
63+
sideEffect: @escaping (AnyPublisher<State, Never>) -> AnyPublisher<Event, Never>) {
64+
self.init(sideEffect: sideEffect)
65+
}
66+
4567
/// Creates a Feedback based on a side effect that takes a generic State as an input.
4668
/// - Parameters:
69+
/// - on: The type of state that should trigger the side effect (forced to AnyState)
4770
/// - strategy: when cancelOnNewState, the current side effect's execution will be canceled
4871
/// - sideEffect: the side effect to execute in the context of the feedback
4972
/// - Returns: the feedback that stands for the side effect
50-
public init(strategy: Feedback.Strategy,
73+
public init(on: AnyState.Type,
74+
strategy: Feedback.Strategy,
5175
willExecuteWithStrategy: @escaping (Feedback.Strategy) -> Void = { _ in },
5276
sideEffect: @escaping (State) -> AnyPublisher<Event, Never>) {
53-
self.init(sideEffect: strategy.apply(on: sideEffect, willExecuteWithStrategy: willExecuteWithStrategy))
77+
self.init(on: AnyState.self, sideEffect: strategy.apply(on: sideEffect, willExecuteWithStrategy: willExecuteWithStrategy))
5478
}
5579

56-
/// Creates a Feedback based on a side effect that takes a concrete State as an input.
80+
/// Creates a Feedback based on a side effect to execute
5781
/// - Parameters:
82+
/// - on: The type of state that should trigger the side effect
83+
/// - sideEffect: the side effect to execute in the context of this feedback
84+
public init<StateType: State>(on: StateType.Type,
85+
sideEffect: @escaping (AnyPublisher<StateType, Never>) -> AnyPublisher<Event, Never>) {
86+
let wrappingSideEffect: (AnyPublisher<State, Never>) -> AnyPublisher<Event, Never> = { states in
87+
sideEffect(states.compactMap { $0 as? StateType }.eraseToAnyPublisher())
88+
}
89+
self.sideEffect = wrappingSideEffect
90+
}
91+
92+
/// Creates a Feedback based on a side effect that takes a generic State as an input.
93+
/// - Parameters:
94+
/// - on: The type of state that should trigger the side effect
5895
/// - strategy: when cancelOnNewState, the current side effect's execution will be canceled
5996
/// - sideEffect: the side effect to execute in the context of the feedback
6097
/// - Returns: the feedback that stands for the side effect
61-
public init<StateType: State>(strategy: Feedback.Strategy,
62-
willExecuteWithStrategy: @escaping (Feedback.Strategy) -> Void = { _ in },
63-
sideEffect: @escaping (StateType) -> AnyPublisher<Event, Never>) {
64-
let wrappingSideEffect: (State) -> AnyPublisher<Event, Never> = { state in
65-
guard let concreteState = state as? StateType else { return Empty().eraseToAnyPublisher() }
66-
return sideEffect(concreteState)
67-
}
68-
69-
self.init(strategy: strategy, willExecuteWithStrategy: willExecuteWithStrategy, sideEffect: wrappingSideEffect)
98+
public init<StateType: State>(on: StateType.Type,
99+
strategy: Feedback.Strategy,
100+
willExecuteWithStrategy: @escaping (Feedback.Strategy) -> Void = { _ in },
101+
sideEffect: @escaping (StateType) -> AnyPublisher<Event, Never>) {
102+
self.init(on: StateType.self, sideEffect: strategy.apply(on: sideEffect, willExecuteWithStrategy: willExecuteWithStrategy))
70103
}
71104
}
72105

Sources/Feedbacks/System/Feedbacks.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,13 @@ public extension Feedbacks {
135135
/// - Parameter perform: the middleware to execute
136136
/// - Returns: the Feedbacks that executes the middleware before executing the side effects
137137
func onStateReceived(_ perform: @escaping (State) -> Void) -> Feedbacks {
138-
let stateReceivedFeedback = Feedback(strategy: .continueOnNewState) { (state: State) in
139-
perform(state)
140-
return Empty().eraseToAnyPublisher()
138+
let stateReceivedFeedback = Feedback { (states: AnyPublisher<State, Never>) in
139+
states
140+
.handleEvents(receiveOutput: perform)
141+
.flatMap { _ in
142+
Empty<Event, Never>().eraseToAnyPublisher()
143+
}
144+
.eraseToAnyPublisher()
141145
}
142146

143147
let newFeedbacks = self.feedbacks + [stateReceivedFeedback]

0 commit comments

Comments
 (0)