Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active January 25, 2026 15:36
Show Gist options
  • Select an option

  • Save fxm90/be62335d987016c84d2f8b3731197c98 to your computer and use it in GitHub Desktop.

Select an option

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)
/// Emits a tuple containing the previous and current elements from the upstream publisher.
///
/// The first emitted tuple has `nil` as the `previous` value because there is no element
/// before the first one.
///
/// ### Example
///
/// ```swift
/// let subscription = (1...5).publisher
/// .pairwise()
/// .sink { print("(\($0.previous), \($0.current))", terminator: " ") }
/// ```
///
/// Output:
/// ```
/// (nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5)
/// ```
///
/// - Returns: A type-erased publisher that emits `(previous, current)` tuples
/// for each element produced by the upstream publisher.
///
/// Based on: <https://stackoverflow.com/a/67133582/3532505>
func pairwise() -> AnyPublisher<Pairwise<Output>, Failure> {
// `scan` requires an initial value, which is `nil` in this case.
// We therefore emit an optional tuple and remove the optional using `compactMap`.
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 when using XCTest

import Combine
import XCTest

final class PairwiseTestCase: XCTestCase {

  func test_pairwise() {
    // Given
    var receivedPairs = [Pairwise<Int>]()

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

    // When
    range.publisher
      .pairwise()
      .sink { previous, current in
        receivedPairs.append(
          Pairwise(previous: previous, current: 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),
    ])
  }
}

// MARK: - Supporting Types

/// Swift does not allow tuples to conform to `Equatable`, so we map the `(previous, current)` tuples
/// emitted by `pairwise()` to this struct in order to make assertions.
///
/// The production implementation uses a `typealias` for the tuple, cause we cannot define a concrete struct
/// inside a protocol extension.
///
/// This test-only struct mirrors the tuple shape and provides `Equatable` conformance.
private struct Pairwise<T: Equatable>: Equatable {
  let previous: T?
  let current: T
}

Related test when using Swift Testing

import Combine
import Testing

@Suite
struct CombinePairwiseTests {

  @Test
  func pairwise() {
    // Given
    var receivedPairs = [Pairwise<Int>]()

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

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

    // Then
    #expect(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),
    ])
  }
}

// MARK: - Supporting Types

/// Swift does not allow tuples to conform to `Equatable`, so we map the `(previous, current)` tuples
/// emitted by `pairwise()` to this struct in order to make assertions.
///
/// The production implementation uses a `typealias` for the tuple, cause we cannot define a concrete struct
/// inside a protocol extension.
///
/// This test-only struct mirrors the tuple shape and provides `Equatable` conformance.
private struct Pairwise<T: Equatable>: Equatable {
  let previous: T?
  let current: T
}

@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