Combine previous value using Combine

12.4k Views Asked by At

How can I rewrite ReactiveSwift/ReactiveCocoa code using Combine framework? I attached screenshot what combinePrevious mean from docs.

let producer = SignalProducer<Int, Never>([1, 2, 3]).combinePrevious(0)
producer.startWithValues { value in
    print(value) // print: (0, 1), (1, 2), (2, 3)
}

enter image description here

3

There are 3 best solutions below

1
New Dev On BEST ANSWER

I'm not completely familiar with ReactiveSwift/ReactiveCocoa, but based on your description, you can use .scan, which seems to be a more general function than combinePrevious.

It takes an initial result - which you can make into a tuple -, and a closure with the stored value and the current value, and returns a new stored value - in your case, a tuple with (previous, current):

let producer = [1,2,3].publisher
                      .scan((0,0)) { ($0.1, $1) }

producer.sink { 
   print($0) 
}
4
Clay Ellis On

These are the custom operators I've come up with (called withPrevious). There are two overloads, one where the initial previous value is nil and the other where you provide the initial previous value so you don't have to deal with optionals.

extension Publisher {

    /// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
    /// The first time the upstream publisher emits an element, the previous element will be `nil`.
    ///
    ///     let range = (1...5)
    ///     cancellable = range.publisher
    ///         .withPrevious()
    ///         .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
    ///      // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ".
    ///
    /// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
    func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
        scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
            .compactMap { $0 }
            .eraseToAnyPublisher()
    }

    /// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is not optional.
    /// The first time the upstream publisher emits an element, the previous element will be the `initialPreviousValue`.
    ///
    ///     let range = (1...5)
    ///     cancellable = range.publisher
    ///         .withPrevious(0)
    ///         .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
    ///      // Prints: "(0, 1) (1, 2) (2, 3) (3, 4) (4, 5) ".
    ///
    /// - Parameter initialPreviousValue: The initial value to use as the "previous" value when the upstream publisher emits for the first time.
    /// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
    func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> {
        scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher()
    }
}

A third overload exists where Output itself is optional which results in previous being a double optional of type Output?? (the scenario that @richy points out in the comments.) This third overload flattens the output so that both previous and current are optional ((previous: Output?, current: Output?) as opposed to (previous: Output??, current: Output?).)

extension Publisher {
    func withPrevious<T>() -> AnyPublisher<(previous: Output, current: Output), Failure> where Output == Optional<T> {
        scan(Optional<(Output, Output)>.none) { ($0?.1, $1) }
            .compactMap { $0 }
            .eraseToAnyPublisher()
    }
}

The difference is subtle (beyond the generic constraint,) so I'm calling it out specifically: Optional<(Output, Output)>.none vs Optional<(Output?, Output)>.none.

0
Ilya Biltuev On

There is a great example by Cocoacasts:

https://cocoacasts.com/combine-essentials-combining-publishers-with-combine-zip-operator

The zip operator can be used to create a publisher that emits the previous element and the current element a publisher emits. We pass the same publisher to the initializer of the Publishers.Zip struct twice, but apply the dropFirst operator to the second publisher. This simply means that the second publisher doesn't emit the first element of the original publisher.

import Combine

let numbers = [1, 2, 3, 4, 5].publisher

Publishers.Zip(numbers, numbers.dropFirst(1))

Usage:

import Combine

let numbers = [1, 2, 3, 4, 5].publisher

Publishers.Zip(numbers, numbers.dropFirst(1))
    .sink(receiveValue: { values in
        print(values)
    })
    
// (1, 2)
// (2, 3)
// (3, 4)
// (4, 5)