11

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

Taras
  • 1,485
  • 1
  • 16
  • 31

3 Answers3

40

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()
    }
}
Clay Ellis
  • 4,960
  • 2
  • 37
  • 45
25

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) 
}
New Dev
  • 48,427
  • 12
  • 87
  • 129
7

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)
Ilya Biltuev
  • 166
  • 1
  • 7