Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active February 17, 2024 02:09
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fxm90/be62335d987016c84d2f8b3731197c98 to your computer and use it in GitHub Desktop.
Save fxm90/be62335d987016c84d2f8b3731197c98 to your computer and use it in GitHub Desktop.
Extension for a Combine-Publisher that returns the current and previous value.
//
// Combine+Pairwise.swift
//
// Created by Felix Mau on 17.05.21.
// Copyright © 2021 Felix Mau. All rights reserved.
//
import Combine
extension Publisher {
typealias Pairwise<T> = (previous: T?, current: T)
/// 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)
/// let subscription = range.publisher
/// .pairwise()
/// .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.
///
/// - Note: Based on <https://stackoverflow.com/a/67133582/3532505>.
func pairwise() -> AnyPublisher<Pairwise<Output>, Failure> {
// `scan()` needs an initial value, which is `nil` in our case.
// Therefore we have to return an optional here and use `compactMap()` below the remove the optional type.
scan(nil) { previousPair, currentElement -> Pairwise<Output>? in
Pairwise(previous: previousPair?.current, current: currentElement)
}
.compactMap { $0 }
.eraseToAnyPublisher()
}
}
@fxm90
Copy link
Author

fxm90 commented May 17, 2021

Related test case:

class PairwiseTestCase: XCTestCase {

    func testPairwise() {
        // Given
        // We can't conform a tuple to `Equatable`, therefore we map the tuples to a struct.
        // We also can't define a struct in a protocol extension, therefore we used a `typealias` in the implementation.
        struct Pairwise<T: Equatable>: Equatable {
            let previous: T?
            let current: T
        }

        var receivedPairs = [Pairwise<Int>]()

        let range = 1 ... 5
        var subscriptions = Set<AnyCancellable>()

        // When
        range.publisher
            .pairwise()
            .sink {
                receivedPairs.append(Pairwise(previous: $0.previous, current: $0.current))
            }
            .store(in: &subscriptions)

        // Then
        XCTAssertEqual(receivedPairs, [
            Pairwise(previous: nil, current: 1),
            Pairwise(previous: 1, current: 2),
            Pairwise(previous: 2, current: 3),
            Pairwise(previous: 3, current: 4),
            Pairwise(previous: 4, current: 5),
        ])
    }
}

@joaquin102
Copy link

Amazing!! Thank you so much for this 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment