Skip to content

Commit 13d7b66

Browse files
authored
Merge pull request #16 from ReactiveCocoa/anders/first-value-after-nil
Deprecate and introduce some new feedback variants
2 parents 88acee4 + ab74ef0 commit 13d7b66

File tree

3 files changed

+331
-60
lines changed

3 files changed

+331
-60
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
5BC88F9D246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */; };
6969
5BC88F9E246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC88F9B246B1CDE00394C63 /* SignalProducer+Loop.swift */; };
7070
5BDEDA3B2473357A00A13013 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; };
71-
5BDEDA3C2473357A00A13013 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; };
7271
5BDEDA3D2473357B00A13013 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; };
7372
5BDEDA3E2473359C00A13013 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */; };
7473
5BDEDA3F2473359D00A13013 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F921B247325C300C1C90E /* EnvironmentValues.swift */; };
@@ -99,6 +98,10 @@
9998
65F8C2972183725900924657 /* Loop.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25CC87AE1F92855300A6EBFC /* Loop.framework */; };
10099
65F8C2CF218378F500924657 /* Loop.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 65F8C26B218371A800924657 /* Loop.framework */; };
101100
65F8C2D0218378F500924657 /* Loop.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 65F8C26B218371A800924657 /* Loop.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
101+
9A01A42824BC78E200075A57 /* FeedbackVariantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A01A42724BC78E200075A57 /* FeedbackVariantTests.swift */; };
102+
9A01A42924BC78E200075A57 /* FeedbackVariantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A01A42724BC78E200075A57 /* FeedbackVariantTests.swift */; };
103+
9A01A42A24BC78E200075A57 /* FeedbackVariantTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A01A42724BC78E200075A57 /* FeedbackVariantTests.swift */; };
104+
9A01A42B24BC7E2E00075A57 /* EnvironmentLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F92132473250E00C1C90E /* EnvironmentLoop.swift */; };
102105
9AD5D42D1F97375E00E6AE5A /* Property+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AD5D42C1F97375E00E6AE5A /* Property+System.swift */; };
103106
9AE181BB1F95A71B00A07551 /* Loop.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25CC87AE1F92855300A6EBFC /* Loop.framework */; };
104107
9AE9563E2186341B005A8C69 /* ReactiveCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9AE9563B21863415005A8C69 /* ReactiveCocoa.framework */; };
@@ -248,6 +251,7 @@
248251
65F8C27A218371AC00924657 /* Loop.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Loop.framework; sourceTree = BUILT_PRODUCTS_DIR; };
249252
65F8C28E2183723F00924657 /* LoopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
250253
65F8C2A22183725900924657 /* LoopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
254+
9A01A42724BC78E200075A57 /* FeedbackVariantTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackVariantTests.swift; sourceTree = "<group>"; };
251255
9AD5D42C1F97375E00E6AE5A /* Property+System.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Property+System.swift"; sourceTree = "<group>"; };
252256
9AE181B61F95A71B00A07551 /* LoopTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
253257
9AE181BA1F95A71B00A07551 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -520,6 +524,7 @@
520524
5898B6D01F97ADDD005EEAEC /* SystemTests.swift */,
521525
9AE181BA1F95A71B00A07551 /* Info.plist */,
522526
250B70DE23FC441300848429 /* FeedbackLoopSystemTests.swift */,
527+
9A01A42724BC78E200075A57 /* FeedbackVariantTests.swift */,
523528
);
524529
path = LoopTests;
525530
sourceTree = "<group>";
@@ -876,6 +881,7 @@
876881
5BC88F9D246B1CDE00394C63 /* SignalProducer+Loop.swift in Sources */,
877882
5BC88F93246B17B200394C63 /* Context.swift in Sources */,
878883
5BAB9751247FFBC10079B532 /* SwiftUIHotSwappableSubscription.swift in Sources */,
884+
9A01A42B24BC7E2E00075A57 /* EnvironmentLoop.swift in Sources */,
879885
585CD87C239E6A3E004BE9CC /* Reducer.swift in Sources */,
880886
65F8C262218371A800924657 /* Property+System.swift in Sources */,
881887
5BC88F89246AFC5900394C63 /* ReactiveSwift+EnqueueTo.swift in Sources */,
@@ -886,7 +892,6 @@
886892
5BC88F8F246B11DE00394C63 /* LoopBox.swift in Sources */,
887893
65761B2723CF20EF004D5506 /* Floodgate.swift in Sources */,
888894
5BC88F99246B191200394C63 /* Loop.swift in Sources */,
889-
5BDEDA3C2473357A00A13013 /* EnvironmentLoop.swift in Sources */,
890895
);
891896
runOnlyForDeploymentPostprocessing = 0;
892897
};
@@ -919,6 +924,7 @@
919924
isa = PBXSourcesBuildPhase;
920925
buildActionMask = 2147483647;
921926
files = (
927+
9A01A42924BC78E200075A57 /* FeedbackVariantTests.swift in Sources */,
922928
65F8C2802183723F00924657 /* SystemTests.swift in Sources */,
923929
250B70E023FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */,
924930
);
@@ -928,6 +934,7 @@
928934
isa = PBXSourcesBuildPhase;
929935
buildActionMask = 2147483647;
930936
files = (
937+
9A01A42A24BC78E200075A57 /* FeedbackVariantTests.swift in Sources */,
931938
65F8C2942183725900924657 /* SystemTests.swift in Sources */,
932939
250B70E123FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */,
933940
);
@@ -937,6 +944,7 @@
937944
isa = PBXSourcesBuildPhase;
938945
buildActionMask = 2147483647;
939946
files = (
947+
9A01A42824BC78E200075A57 /* FeedbackVariantTests.swift in Sources */,
940948
5898B6D11F97ADDD005EEAEC /* SystemTests.swift in Sources */,
941949
250B70DF23FC441300848429 /* FeedbackLoopSystemTests.swift in Sources */,
942950
);

Loop/Public/FeedbackLoop.swift

Lines changed: 144 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -194,26 +194,6 @@ extension Loop {
194194
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
195195
return Feedback(compactingEvents: transform, effects: effects)
196196
}
197-
198-
/// Creates a Feedback which re-evaluates the given effect every time the
199-
/// state changes, and the transform consequentially yields a new value
200-
/// distinct from the last yielded value.
201-
///
202-
/// If the previous effect is still alive when a new one is about to start,
203-
/// the previous one would automatically be cancelled.
204-
///
205-
/// - parameters:
206-
/// - transform: The transform to apply on the state.
207-
/// - effects: The side effect accepting transformed values produced by
208-
/// `transform` and yielding events that eventually affect
209-
/// the state.
210-
@available(*, deprecated, renamed:"init(skippingRepeatedState:effects:)")
211-
public init<Control: Equatable, Effect: SignalProducerConvertible>(
212-
skippingRepeated transform: @escaping (State) -> Control?,
213-
effects: @escaping (Control) -> Effect
214-
) where Effect.Value == Event, Effect.Error == Never {
215-
self.init(skippingRepeatedState: transform, effects: effects)
216-
}
217197

218198
public init<Control: Equatable, Effect: SignalProducerConvertible>(
219199
skippingRepeatedState transform: @escaping (State) -> Control?,
@@ -301,7 +281,6 @@ extension Loop {
301281
/// - effects: The side effect accepting transformed values produced by
302282
/// `transform` and yielding events that eventually affect
303283
/// the state.
304-
305284
public static func lensing<Control, Effect: SignalProducerConvertible>(
306285
state transform: @escaping (State) -> Control?,
307286
effects: @escaping (Control) -> Effect
@@ -315,61 +294,104 @@ extension Loop {
315294
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
316295
Feedback(extractingPayload: transform, effects: effects)
317296
}
318-
319-
/// Creates a Feedback which re-evaluates the given effect every time the
320-
/// given predicate passes.
297+
298+
/// Create a Feedback which (re)starts the effect every time `transform` emits a non-nil value after a sequence
299+
/// of `nil`, and ignore all the non-nil value afterwards. It does so until `transform` starts emitting a `nil`,
300+
/// at which point the feedback cancels any outstanding effect.
321301
///
322-
/// If the previous effect is still alive when a new one is about to start,
323-
/// the previous one would automatically be cancelled.
302+
/// - parameters:
303+
/// - transform: The transform to select a specific part of the state, or to cancel the outstanding effect
304+
/// by returning `nil`.
305+
/// - effects: The side effect accepting the first non-nil value produced by `transform`, and yielding events
306+
/// that eventually affect the state.
307+
public init<Value, Effect: SignalProducerConvertible>(
308+
firstValueAfterNil transform: @escaping (State) -> Value?,
309+
effects: @escaping (Value) -> Effect
310+
) where Effect.Value == Event, Effect.Error == Never {
311+
self.init(
312+
compacting: { state in
313+
state
314+
.scan(into: (false, nil)) { (temp: inout (lastWasNil: Bool, output: NilEdgeTransition<Value>?), state: State) in
315+
let result = transform(state)
316+
temp.output = nil
317+
318+
switch (temp.lastWasNil, result) {
319+
case (true, .none), (false, .some):
320+
return
321+
case let (true, .some(value)):
322+
temp.lastWasNil = false
323+
temp.output = .populated(value)
324+
case (false, .none):
325+
temp.lastWasNil = true
326+
temp.output = .cleared
327+
}
328+
}
329+
.compactMap { $0.output }
330+
},
331+
effects: { transition -> SignalProducer<Event, Never> in
332+
switch transition {
333+
case let .populated(value):
334+
return effects(value).producer
335+
case .cleared:
336+
return .empty
337+
}
338+
}
339+
)
340+
}
341+
342+
/// Create a feedback which (re)starts the effect every time `transform` emits a non-nil value after a sequence
343+
/// of `nil`, and ignore all the non-nil value afterwards. It does so until `transform` starts emitting a `nil`,
344+
/// at which point the feedback cancels any outstanding effect.
324345
///
325346
/// - parameters:
326-
/// - predicate: The predicate to apply on the state.
327-
/// - effects: The side effect accepting the state and yielding events
347+
/// - transform: The transform to select a specific part of the state, or to cancel the outstanding effect
348+
/// by returning `nil`.
349+
/// - effects: The side effect accepting the first non-nil value produced by `transform`, and yielding events
328350
/// that eventually affect the state.
351+
public static func firstValueAfterNil<Value, Effect: SignalProducerConvertible>(
352+
_ transform: @escaping (State) -> Value?,
353+
effects: @escaping (Value) -> Effect
354+
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
355+
self.init(firstValueAfterNil: transform, effects: effects)
356+
}
357+
358+
/// Creates a Feedback which evaluates the given effect when the predicate transitions to `true`, and
359+
/// cancels the outstanding effect when the predicate transitions to `false`.
360+
///
361+
/// In other words, this variant treats the output of `predicate` as a binary signal. It starts the effect when
362+
/// there is a positive edge, and cancels the outstanding effect (if any) when there is a negative edge.
363+
///
364+
/// - parameters:
365+
/// - predicate: The predicate to indicate whether effects should start or be cancelled.
366+
/// - effects: The side effect accepting the state and yielding events that eventually affect the state.
329367
public init<Effect: SignalProducerConvertible>(
330-
predicate: @escaping (State) -> Bool,
368+
whenBecomesTrue predicate: @escaping (State) -> Bool,
331369
effects: @escaping (State) -> Effect
332370
) where Effect.Value == Event, Effect.Error == Never {
333371
self.init(
334-
compactingState: { $0 },
372+
firstValueAfterNil: { predicate($0) ? $0 : nil },
335373
effects: { state -> SignalProducer<Event, Never> in
336-
predicate(state) ? effects(state).producer : .empty
374+
effects(state).producer
337375
}
338376
)
339377
}
340-
341-
/// Creates a Feedback which re-evaluates the given effect every time the
342-
/// given predicate passes.
378+
379+
/// Creates a Feedback which evaluates the given effect when the predicate transitions to `true`, and
380+
/// cancels the outstanding effect when the predicate transitions to `false`.
343381
///
344-
/// If the previous effect is still alive when a new one is about to start,
345-
/// the previous one would automatically be cancelled.
382+
/// In other words, this variant treats the output of `predicate` as a binary signal. It starts the effect when
383+
/// there is a positive edge, and cancels the outstanding effect (if any) when there is a negative edge.
346384
///
347385
/// - parameters:
348-
/// - predicate: The predicate to apply on the state.
349-
/// - effects: The side effect accepting the state and yielding events
350-
/// that eventually affect the state.
351-
public static func predicate<Effect: SignalProducerConvertible>(
352-
state predicate: @escaping (State) -> Bool,
386+
/// - predicate: The predicate to indicate whether effects should start or be cancelled.
387+
/// - effects: The side effect accepting the state and yielding events that eventually affect the state.
388+
public static func whenBecomesTrue<Effect: SignalProducerConvertible>(
389+
_ predicate: @escaping (State) -> Bool,
353390
effects: @escaping (State) -> Effect
354391
) -> Feedback where Effect.Value == Event, Effect.Error == Never {
355-
Feedback(predicate: predicate, effects: effects)
392+
self.init(whenBecomesTrue: predicate, effects: effects)
356393
}
357-
358-
/// Creates a Feedback which re-evaluates the given effect every time the
359-
/// state changes.
360-
///
361-
/// If the previous effect is still alive when a new one is about to start,
362-
/// the previous one would automatically be cancelled.
363-
///
364-
/// - parameters:
365-
/// - effects: The side effect accepting the state and yielding events
366-
/// that eventually affect the state.
367-
public init<Effect: SignalProducerConvertible>(
368-
effects: @escaping (State) -> Effect
369-
) where Effect.Value == Event, Effect.Error == Never {
370-
self.init(compactingState: { $0 }, effects: effects)
371-
}
372-
394+
373395
/// Creates a Feedback which re-evaluates the given effect every time the
374396
/// state changes with the Event that caused the change.
375397
///
@@ -443,3 +465,67 @@ extension Loop {
443465
}
444466
}
445467
}
468+
469+
extension Loop.Feedback {
470+
/// Creates a Feedback which re-evaluates the given effect every time the
471+
/// state changes, and the transform consequentially yields a new value
472+
/// distinct from the last yielded value.
473+
///
474+
/// If the previous effect is still alive when a new one is about to start,
475+
/// the previous one would automatically be cancelled.
476+
///
477+
/// - parameters:
478+
/// - transform: The transform to apply on the state.
479+
/// - effects: The side effect accepting transformed values produced by
480+
/// `transform` and yielding events that eventually affect
481+
/// the state.
482+
@available(*, deprecated, renamed:"init(skippingRepeatedState:effects:)")
483+
public init<Control: Equatable, Effect: SignalProducerConvertible>(
484+
skippingRepeated transform: @escaping (State) -> Control?,
485+
effects: @escaping (Control) -> Effect
486+
) where Effect.Value == Event, Effect.Error == Never {
487+
self.init(skippingRepeatedState: transform, effects: effects)
488+
}
489+
490+
/// Creates a Feedback which re-evaluates the given effect every time the
491+
/// given predicate passes.
492+
///
493+
/// If the previous effect is still alive when a new one is about to start,
494+
/// the previous one would automatically be cancelled.
495+
///
496+
/// - parameters:
497+
/// - predicate: The predicate to apply on the state.
498+
/// - effects: The side effect accepting the state and yielding events
499+
/// that eventually affect the state.
500+
@available(*, deprecated, message:"Use `Feedback.init(whenBecomesTrue:effects:)`, or other more appropriate variants.")
501+
public init<Effect: SignalProducerConvertible>(
502+
predicate: @escaping (State) -> Bool,
503+
effects: @escaping (State) -> Effect
504+
) where Effect.Value == Event, Effect.Error == Never {
505+
self.init(compacting: { $0 },
506+
effects: { state -> SignalProducer<Event, Never> in
507+
predicate(state) ? effects(state).producer : .empty
508+
})
509+
}
510+
511+
/// Creates a Feedback which re-evaluates the given effect every time the
512+
/// state changes.
513+
///
514+
/// If the previous effect is still alive when a new one is about to start,
515+
/// the previous one would automatically be cancelled.
516+
///
517+
/// - parameters:
518+
/// - effects: The side effect accepting the state and yielding events
519+
/// that eventually affect the state.
520+
@available(*, deprecated, message:"Use `Feedback.init(whenBecomesTrue:effects:)`, or other more appropriate variants.")
521+
public init<Effect: SignalProducerConvertible>(
522+
effects: @escaping (State) -> Effect
523+
) where Effect.Value == Event, Effect.Error == Never {
524+
self.init(compacting: { $0 }, effects: effects)
525+
}
526+
}
527+
528+
private enum NilEdgeTransition<Value> {
529+
case populated(Value)
530+
case cleared
531+
}

0 commit comments

Comments
 (0)