@@ -4,31 +4,102 @@ extension Loop {
4
4
public struct Feedback {
5
5
let events : ( _ state: SignalProducer < State , Never > , _ output: FeedbackEventConsumer < Event > ) -> Disposable
6
6
7
- public init (
8
- events: @escaping (
9
- _ state: SignalProducer < State , Never > ,
10
- _ output: FeedbackEventConsumer < Event >
11
- ) -> Disposable
7
+ /// Private designated initializer. See the public designated initializer below.
8
+ fileprivate init (
9
+ startWith events: @escaping ( _ state: SignalProducer < State , Never > , _ output: FeedbackEventConsumer < Event > ) -> Disposable
12
10
) {
13
11
self . events = events
14
12
}
15
13
16
14
/// Creates a custom Feedback, with the complete liberty of defining the data flow.
17
15
///
18
- /// - important: While you may respond to state changes in whatever ways you prefer, you **must** enqueue produced
19
- /// events using the `SignalProducer.enqueue(to:)` operator to the `FeedbackEventConsumer` provided
20
- /// to you. Otherwise, the feedback loop will not be able to pick up and process your events.
16
+ /// Consider using the standard `Feedback` variants, before deriving down to use this desginated initializer.
17
+ ///
18
+ /// Events must be explicitly enqueued using `SignalProducer.enqueue(to:)` with the `FeedbackEventConsumer`
19
+ /// provided to the setup closure. `enqueue(to:)` respects producer cancellation and removes outstanding events
20
+ /// from the loop internal event queue.
21
+ ///
22
+ /// This is useful if you wish to discard events when the state changes in certain ways. For example,
23
+ /// `Feedback(skippingRepeated:effects:)` enqueues events inside `flatMap(.latest)`, so that unprocessed events
24
+ /// are automatically removed when the inner producer has switched.
25
+ ///
26
+ /// ## State producer in the `setup` closure
27
+ /// The setup closure provides you a `state` producer — it replays the latest state at starting time, and then
28
+ /// publishes all state changes.
29
+ ///
30
+ /// Loop guarantees only that this `state` producer is **eventually consistent** with events emitted by your
31
+ /// feedback. This means you should not make any strong assumptions on events you enqueued being immediately
32
+ /// reflected by `state`.
33
+ ///
34
+ /// For example, if you start the `state` producer again, synchronously after enqueuing an event, the event
35
+ /// may not have been processed yet, and therefore the assertion would fail:
36
+ /// ```swift
37
+ /// Feedback { state, output in
38
+ /// state
39
+ /// .filter { $0.apples.isEmpty == false }
40
+ /// .map(value: Event.eatAllApples)
41
+ /// .take(first: 1)
42
+ /// .concat(
43
+ /// state
44
+ /// .take(first: 1)
45
+ /// .on(value: { state in
46
+ /// guard state.apples.isEmpty else { return }
47
+ ///
48
+ /// // ❌🙅♀️ No guarantee that this is true.
49
+ /// fatalError("It should have eaten all the apples!")
50
+ /// })
51
+ /// )
52
+ /// .enqueue(to: output)
53
+ /// }
54
+ /// ```
55
+ ///
56
+ /// You can however expect it to be eventually consistent:
57
+ /// ```swift
58
+ /// Feedback { state, output in
59
+ /// state
60
+ /// .filter { $0.apples.isEmpty == false }
61
+ /// .map(value: Event.eatAllApples)
62
+ /// .take(first: 1)
63
+ /// .concat(
64
+ /// state
65
+ /// .filter { $0.apples.isEmpty } // ℹ️ Watching specifically for the ideal state.
66
+ /// .take(first: 1)
67
+ /// .on(value: { state in
68
+ /// guard state.apples.isEmpty else { return }
69
+ ///
70
+ /// // ✅👍 We would eventually observe this, when the loop event queue
71
+ /// // has caught up with `.eatAppleApples` we enqueued earlier.
72
+ /// fatalError("It should have eaten all the apples!")
73
+ /// })
74
+ /// )
75
+ /// .enqueue(to: output)
76
+ /// }
77
+ /// ```
21
78
///
22
79
/// - parameters:
23
80
/// - setup: The setup closure to construct a data flow producing events in respond to changes from `state`,
24
81
/// and having them consumed by `output` using the `SignalProducer.enqueue(to:)` operator.
25
- public static func custom (
26
- _ setup : @escaping (
82
+ public init (
83
+ events : @escaping (
27
84
_ state: SignalProducer < State , Never > ,
28
85
_ output: FeedbackEventConsumer < Event >
29
- ) -> Disposable
30
- ) -> Feedback {
31
- return Feedback ( events: setup)
86
+ ) -> SignalProducer < Never , Never >
87
+ ) {
88
+ self . events = { events ( $0, $1) . start ( ) }
89
+ }
90
+
91
+ /// Creates a Feedback that observes an external producer and maps it to an event.
92
+ ///
93
+ /// - parameters:
94
+ /// - setup: The setup closure to construct a data flow producing events in respond to changes from `state`,
95
+ /// and having them consumed by `output` using the `SignalProducer.enqueue(to:)` operator.
96
+ public init < Values: SignalProducerConvertible > (
97
+ source: Values ,
98
+ as transform: @escaping ( Values . Value ) -> Event
99
+ ) where Values. Error == Never {
100
+ self . init { _, output in
101
+ source. producer. map ( transform) . enqueueNonCancelling ( to: output)
102
+ }
32
103
}
33
104
34
105
/// Creates a Feedback which re-evaluates the given effect every time the
@@ -133,9 +204,7 @@ extension Loop {
133
204
134
205
public static var input : ( feedback: Feedback , observer: ( Event ) -> Void ) {
135
206
let pipe = Signal < Event , Never > . pipe ( )
136
- let feedback = Feedback . custom { ( state, consumer) -> Disposable in
137
- pipe. output. producer. enqueue ( to: consumer) . start ( )
138
- }
207
+ let feedback = Feedback ( source: pipe. output, as: { $0 } )
139
208
return ( feedback, pipe. input. send)
140
209
}
141
210
@@ -144,23 +213,45 @@ extension Loop {
144
213
value: KeyPath < State , LocalState > ,
145
214
event: @escaping ( LocalEvent ) -> Event
146
215
) -> Feedback {
147
- return Feedback . custom { ( state, consumer) -> Disposable in
216
+ return Feedback ( startWith : { ( state, consumer) in
148
217
return feedback. events (
149
218
state. map ( value) ,
150
219
consumer. pullback ( event)
151
220
)
152
- }
221
+ } )
153
222
}
154
223
155
224
public static func combine( _ feedbacks: Loop < State , Event > . Feedback ... ) -> Feedback {
156
- return . custom { ( state, consumer) -> Disposable in
225
+ return Feedback ( startWith : { ( state, consumer) in
157
226
return feedbacks. map { ( feedback) in
158
227
feedback. events ( state, consumer)
159
228
}
160
229
. reduce ( into: CompositeDisposable ( ) ) { ( composite, disposable) in
161
230
composite += disposable
162
231
}
163
- }
232
+ } )
164
233
}
165
234
}
166
235
}
236
+
237
+ extension Loop . Feedback {
238
+ @available ( * , deprecated, renamed: " init(_:) " )
239
+ public static func custom(
240
+ _ setup: @escaping (
241
+ _ state: SignalProducer < State , Never > ,
242
+ _ output: FeedbackEventConsumer < Event >
243
+ ) -> Disposable
244
+ ) -> Loop . Feedback {
245
+ return FeedbackLoop . Feedback ( events: setup)
246
+ }
247
+
248
+ @available ( * , deprecated, renamed: " init(_:) " )
249
+ public init (
250
+ events: @escaping (
251
+ _ state: SignalProducer < State , Never > ,
252
+ _ output: FeedbackEventConsumer < Event >
253
+ ) -> Disposable
254
+ ) {
255
+ self . events = { events ( $0. producer, $1) }
256
+ }
257
+ }
0 commit comments