Skip to content

Commit 4c59de4

Browse files
committed
Feedback designated initializer change.
1 parent 63dd88b commit 4c59de4

File tree

4 files changed

+162
-35
lines changed

4 files changed

+162
-35
lines changed

Loop/FeedbackLoop.swift

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,51 @@ extension FeedbackLoop {
5252
public struct Feedback {
5353
let events: (_ state: SignalProducer<State, Never>, _ output: FeedbackEventConsumer<Event>) -> Disposable
5454

55-
public init(
56-
events: @escaping (
57-
_ state: SignalProducer<State, Never>,
58-
_ output: FeedbackEventConsumer<Event>
59-
) -> Disposable
55+
/// Private designated initializer. See the public designated initializer below.
56+
fileprivate init(
57+
startWith events: @escaping (_ state: SignalProducer<State, Never>, _ output: FeedbackEventConsumer<Event>) -> Disposable
6058
) {
6159
self.events = events
6260
}
6361

6462
/// Creates a custom Feedback, with the complete liberty of defining the data flow.
6563
///
66-
/// - important: While you may respond to state changes in whatever ways you prefer, you **must** enqueue produced
67-
/// events using the `SignalProducer.enqueue(to:)` operator to the `FeedbackEventConsumer` provided
68-
/// to you. Otherwise, the feedback loop will not be able to pick up and process your events.
64+
/// Consider using the standard `Feedback` variants, before deriving down to use this desginated initializer.
65+
///
66+
/// Events must be explicitly enqueued using `SignalProducer.enqueue(to:)` with the `FeedbackEventConsumer`
67+
/// provided to the setup closure. `enqueue(to:)` respects producer cancellation and removes outstanding events
68+
/// from the loop internal event queue.
69+
///
70+
/// This is useful if you wish to discard events when the state changes in certain ways. For example,
71+
/// `Feedback(skippingRepeated:effects:)` enqueues events inside `flatMap(.latest)`, so that unprocessed events
72+
/// are automatically removed when the inner producer has switched.
73+
///
74+
/// - important: The `state` producer provided to the setup closure **does not** replay the current state.
6975
///
7076
/// - parameters:
7177
/// - setup: The setup closure to construct a data flow producing events in respond to changes from `state`,
7278
/// and having them consumed by `output` using the `SignalProducer.enqueue(to:)` operator.
73-
public static func custom(
74-
_ setup: @escaping (
79+
public init(
80+
events: @escaping (
7581
_ state: SignalProducer<State, Never>,
7682
_ output: FeedbackEventConsumer<Event>
77-
) -> Disposable
78-
) -> Feedback {
79-
return Feedback(events: setup)
83+
) -> SignalProducer<Never, Never>
84+
) {
85+
self.events = { events($0, $1).start() }
86+
}
87+
88+
/// Creates a Feedback that observes an external producer and maps it to an event.
89+
///
90+
/// - parameters:
91+
/// - setup: The setup closure to construct a data flow producing events in respond to changes from `state`,
92+
/// and having them consumed by `output` using the `SignalProducer.enqueue(to:)` operator.
93+
public init<Values: SignalProducerConvertible>(
94+
source: Values,
95+
as transform: @escaping (Values.Value) -> Event
96+
) where Values.Error == Never {
97+
self.init { _, output in
98+
source.producer.map(transform).enqueueNonCancelling(to: output)
99+
}
80100
}
81101

82102
/// Creates a Feedback which re-evaluates the given effect every time the
@@ -181,9 +201,7 @@ extension FeedbackLoop {
181201

182202
public static var input: (feedback: Feedback, observer: (Event) -> Void) {
183203
let pipe = Signal<Event, Never>.pipe()
184-
let feedback = Feedback.custom { (state, consumer) -> Disposable in
185-
pipe.output.producer.enqueue(to: consumer).start()
186-
}
204+
let feedback = Feedback(source: pipe.output, as: { $0 })
187205
return (feedback, pipe.input.send)
188206
}
189207

@@ -192,23 +210,45 @@ extension FeedbackLoop {
192210
value: KeyPath<State, LocalState>,
193211
event: @escaping (LocalEvent) -> Event
194212
) -> Feedback {
195-
return Feedback.custom { (state, consumer) -> Disposable in
213+
return Feedback(startWith: { (state, consumer) in
196214
return feedback.events(
197215
state.map(value),
198216
consumer.pullback(event)
199217
)
200-
}
218+
})
201219
}
202220

203221
public static func combine(_ feedbacks: FeedbackLoop<State, Event>.Feedback...) -> Feedback {
204-
return .custom { (state, consumer) -> Disposable in
222+
return Feedback(startWith: { (state, consumer) in
205223
return feedbacks.map { (feedback) in
206224
feedback.events(state, consumer)
207225
}
208226
.reduce(into: CompositeDisposable()) { (composite, disposable) in
209227
composite += disposable
210228
}
211-
}
229+
})
212230
}
213231
}
214232
}
233+
234+
extension FeedbackLoop.Feedback {
235+
@available(*, deprecated, renamed:"init(_:)")
236+
public static func custom(
237+
_ setup: @escaping (
238+
_ state: SignalProducer<State, Never>,
239+
_ output: FeedbackEventConsumer<Event>
240+
) -> Disposable
241+
) -> FeedbackLoop.Feedback {
242+
return FeedbackLoop.Feedback(events: setup)
243+
}
244+
245+
@available(*, deprecated, renamed:"init(_:)")
246+
public init(
247+
events: @escaping (
248+
_ state: SignalProducer<State, Never>,
249+
_ output: FeedbackEventConsumer<Event>
250+
) -> Disposable
251+
) {
252+
self.events = { events($0.producer, $1) }
253+
}
254+
}

Loop/Floodgate.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,16 +105,3 @@ final class Floodgate<State, Event>: FeedbackEventConsumer<Event> {
105105
changeObserver.send(value: state)
106106
}
107107
}
108-
109-
extension SignalProducer where Error == Never {
110-
public func enqueue(to consumer: FeedbackEventConsumer<Value>) -> SignalProducer<Never, Never> {
111-
SignalProducer<Never, Never> { observer, lifetime in
112-
let token = Token()
113-
114-
lifetime += self.startWithValues { event in
115-
consumer.process(event, for: token)
116-
}
117-
lifetime.observeEnded { consumer.dequeueAllEvents(for: token) }
118-
}
119-
}
120-
}

Loop/ReactiveSwift+FeedbackLoop.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import ReactiveSwift
2+
3+
extension Signal where Error == Never {
4+
/// Enqueue all received values to the given `FeedbackEventConsumer`.
5+
///
6+
/// - note: This converts the `Signal` to be a `SignalProducer<Never, Never>` accepted by `Feedback`.
7+
public func enqueue(to consumer: FeedbackEventConsumer<Value>) -> SignalProducer<Never, Never> {
8+
producer.enqueue(to: consumer)
9+
}
10+
}
11+
12+
extension SignalProducer where Error == Never {
13+
/// Enqueue all received values to the given `FeedbackEventConsumer`.
14+
///
15+
/// If the producer is interrupted, e.g. explicitly by users, or by an operator like `flatMap(.latest)`, unprocessed
16+
/// events would be removed from the loop internal event queue.
17+
public func enqueue(to consumer: FeedbackEventConsumer<Value>) -> SignalProducer<Never, Never> {
18+
SignalProducer<Never, Never> { observer, lifetime in
19+
let token = Token()
20+
21+
lifetime += self.startWithValues { event in
22+
consumer.process(event, for: token)
23+
}
24+
25+
lifetime.observeEnded { consumer.dequeueAllEvents(for: token) }
26+
}
27+
}
28+
29+
internal func enqueueNonCancelling(to consumer: FeedbackEventConsumer<Value>) -> SignalProducer<Never, Never> {
30+
SignalProducer<Never, Never> { observer, lifetime in
31+
let token = Token()
32+
33+
lifetime += self.startWithValues { event in
34+
consumer.process(event, for: token)
35+
}
36+
}
37+
}
38+
}

LoopTests/FeedbackLoopSystemTests.swift

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ class FeedbackLoopSystemTests: XCTestCase {
5959
reduce: { (state, event) in
6060
state += event
6161
},
62-
feedbacks: feedback1, feedback2)
62+
feedbacks: feedback1, feedback2
63+
)
6364

6465
var result: [String]!
6566
system.take(first: 5)
@@ -186,7 +187,6 @@ class FeedbackLoopSystemTests: XCTestCase {
186187
.map(value: "_event")
187188
.on(terminated: { semaphore.signal() })
188189
.enqueue(to: output)
189-
.start()
190190
}
191191
]
192192
)
@@ -242,4 +242,66 @@ class FeedbackLoopSystemTests: XCTestCase {
242242

243243
expect(result).toEventually(equal(expected))
244244
}
245+
246+
func test_external_source_events_are_not_cancelled_when_source_completes() {
247+
enum Event {
248+
case increment(by: Int)
249+
case timeConsumingWork
250+
}
251+
252+
let semaphore = DispatchSemaphore(value: 0)
253+
254+
let (increments, incrementObserver) = Signal<Int, Never>.pipe()
255+
let (workTrigger, workTriggerObserver) = Signal<Void, Never>.pipe()
256+
257+
let system = SignalProducer<Int, Never>.feedbackLoop(
258+
initial: 0,
259+
reduce: { (state, event) in
260+
switch event {
261+
case let .increment(steps):
262+
state += steps
263+
264+
case .timeConsumingWork:
265+
semaphore.wait()
266+
}
267+
},
268+
feedbacks: [
269+
FeedbackLoop.Feedback(source: increments, as: Event.increment(by:)),
270+
FeedbackLoop.Feedback(source: workTrigger, as: { .timeConsumingWork })
271+
]
272+
)
273+
274+
var results: [Int] = []
275+
276+
system.startWithValues { value in
277+
results.append(value)
278+
}
279+
280+
expect(results) == [0]
281+
282+
incrementObserver.send(value: 1)
283+
incrementObserver.send(value: 2)
284+
incrementObserver.send(value: 3)
285+
expect(results) == [0, 1, 3, 6]
286+
287+
waitUntil { done in
288+
DispatchQueue.global(qos: .userInteractive).async {
289+
done()
290+
workTriggerObserver.send(value: ())
291+
}
292+
}
293+
294+
// Sleep for 500us so that we continue the assertions after `workTriggerObserver.send(value: ())` is invoked.
295+
usleep(500)
296+
297+
incrementObserver.send(value: 1)
298+
incrementObserver.send(value: 2)
299+
incrementObserver.send(value: 3)
300+
incrementObserver.sendCompleted()
301+
302+
// Allow the reducer running in background to proceed.
303+
semaphore.signal()
304+
305+
expect(results).toEventually(equal([0, 1, 3, 6, 6, 7, 9, 12]))
306+
}
245307
}

0 commit comments

Comments
 (0)