diff --git a/CHANGELOG.md b/CHANGELOG.md index 413a26320..ae6abb93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # master *Please add new entries at the top.* +1. `combinePrevious` for `Signal` and `SignalProducer` no longer requires an initial value. The first tuple would be emitted as soon as the second value is received by the operator if no initial value is given. (#445, kudos to @andersio) + 1. Fixed an impedance mismatch in the `Signal` internals that caused heap corruptions. (#449, kudos to @gparker42) 1. In Swift 3.2 or later, you may create `BindingTarget` for a key path of a specific object. (#440, kudos to @andersio) diff --git a/Sources/Signal.swift b/Sources/Signal.swift index d5e4a19d0..0a0738b82 100644 --- a/Sources/Signal.swift +++ b/Sources/Signal.swift @@ -1457,11 +1457,47 @@ extension Signal { /// - returns: A signal that sends tuples that contain previous and current /// sent values of `self`. public func combinePrevious(_ initial: Value) -> Signal<(Value, Value), Error> { - return scan((initial, initial)) { previousCombinedValues, newValue in - return (previousCombinedValues.1, newValue) - } + return combinePrevious(initial: initial) + } + + /// Forward events from `self` with history: values of the returned signal + /// are a tuples whose first member is the previous value and whose second member + /// is the current value. + /// + /// The returned `Signal` would not emit any tuple until it has received at least two + /// values. + /// + /// - returns: A signal that sends tuples that contain previous and current + /// sent values of `self`. + public func combinePrevious() -> Signal<(Value, Value), Error> { + return combinePrevious(initial: nil) } + /// Implementation detail of `combinePrevious`. A default argument of a `nil` initial + /// is deliberately avoided, since in the case of `Value` being an optional, the + /// `nil` literal would be materialized as `Optional.none` instead of `Value`, + /// thus changing the semantic. + private func combinePrevious(initial: Value?) -> Signal<(Value, Value), Error> { + return Signal<(Value, Value), Error> { observer in + var previous = initial + + return self.observe { event in + switch event { + case let .value(value): + if let previous = previous { + observer.send(value: (previous, value)) + } + previous = value + case .completed: + observer.sendCompleted() + case let .failed(error): + observer.send(error: error) + case .interrupted: + observer.sendInterrupted() + } + } + } + } /// Combine all values from `self`, and forward only the final accumuated result. /// diff --git a/Sources/SignalProducer.swift b/Sources/SignalProducer.swift index 6676f4905..5707cdd16 100644 --- a/Sources/SignalProducer.swift +++ b/Sources/SignalProducer.swift @@ -978,21 +978,34 @@ extension SignalProducer { return liftRight(Signal.skip(until:))(trigger.producer) } - /// Forward events from `self` with history: values of the returned producer - /// are a tuple whose first member is the previous value and whose second - /// member is the current value. `initial` is supplied as the first member - /// when `self` sends its first value. + /// Forward events from `self` with history: values of the returned signal + /// are a tuples whose first member is the previous value and whose second member + /// is the current value. `initial` is supplied as the first member when `self` + /// sends its first value. /// /// - parameters: /// - initial: A value that will be combined with the first value sent by /// `self`. /// - /// - returns: A producer that sends tuples that contain previous and - /// current sent values of `self`. + /// - returns: A signal that sends tuples that contain previous and current + /// sent values of `self`. public func combinePrevious(_ initial: Value) -> SignalProducer<(Value, Value), Error> { return lift { $0.combinePrevious(initial) } } + /// Forward events from `self` with history: values of the produced signal + /// are a tuples whose first member is the previous value and whose second member + /// is the current value. + /// + /// The produced `Signal` would not emit any tuple until it has received at least two + /// values. + /// + /// - returns: A producer that sends tuples that contain previous and current + /// sent values of `self`. + public func combinePrevious() -> SignalProducer<(Value, Value), Error> { + return lift { $0.combinePrevious() } + } + /// Combine all values from `self`, and forward the final result. /// /// See `scan(_:_:)` if the resulting producer needs to forward also the partial diff --git a/Tests/ReactiveSwiftTests/SignalSpec.swift b/Tests/ReactiveSwiftTests/SignalSpec.swift index 4a68389ad..032f87f76 100755 --- a/Tests/ReactiveSwiftTests/SignalSpec.swift +++ b/Tests/ReactiveSwiftTests/SignalSpec.swift @@ -2756,6 +2756,7 @@ class SignalSpec: QuickSpec { } describe("combinePrevious") { + var signal: Signal! var observer: Signal.Observer! let initialValue: Int = 0 var latestValues: (Int, Int)? @@ -2763,12 +2764,13 @@ class SignalSpec: QuickSpec { beforeEach { latestValues = nil - let (signal, baseObserver) = Signal.pipe() - observer = baseObserver - signal.combinePrevious(initialValue).observeValues { latestValues = $0 } + let (baseSignal, baseObserver) = Signal.pipe() + (signal, observer) = (baseSignal, baseObserver) } - it("should forward the latest value with previous value") { + it("should forward the latest value with previous value with an initial value") { + signal.combinePrevious(initialValue).observeValues { latestValues = $0 } + expect(latestValues).to(beNil()) observer.send(value: 1) @@ -2779,6 +2781,19 @@ class SignalSpec: QuickSpec { expect(latestValues?.0) == 1 expect(latestValues?.1) == 2 } + + it("should forward the latest value with previous value without any initial value") { + signal.combinePrevious().observeValues { latestValues = $0 } + + expect(latestValues).to(beNil()) + + observer.send(value: 1) + expect(latestValues).to(beNil()) + + observer.send(value: 2) + expect(latestValues?.0) == 1 + expect(latestValues?.1) == 2 + } } describe("combineLatest") {