Combine and AsyncSequence (with version cues)

How to bridge publishers to async/await, manage backpressure, and handle cancellation.

Version Quick Reference

  • iOS 13 / Swift 5.1: Combine introduced with Publisher, operators, and schedulers.
  • iOS 15 / Swift 5.5: AsyncSequence and Publisher.values bridge Combine to async/await.
  • Swift 6+: stricter concurrency checking surfaces Sendable and actor-isolation issues in custom publishers/operators.

Bridging Combine to Async/Await

  • Use publisher.values (iOS 15+) to consume a publisher as an AsyncSequence.
1
2
3
for await value in publisher.values {
    handle(value)
}
  • For single values, prefer try await publisher.first(where:) or async variants instead of promise-style bridges.

Cancellation and Backpressure

  • Cancellation in Combine maps to Task cancellation in async bridges. Respect Task.isCancelled when producing values manually.
  • Backpressure is automatic for many operators, but custom Publisher implementations should consider demand. When bridging to async, the consumer naturally pulls values; avoid buffering unboundedly.

Error Handling

  • Combine uses Failure associated type; Never means no errors. When bridging, failures throw in async contexts.
1
2
3
4
5
6
7
do {
    for try await value in fallible.values {
        handle(value)
    }
} catch {
    // handle Combine Failure here
}

Common Patterns

  • UI: map publishers to async functions in SwiftUI task {} blocks; cancel tasks on disappear.
  • Testing: use AsyncStream to feed deterministic values into async code, or use TestScheduler for pure Combine pipelines.

Replacing Combine with AsyncSequence

  • For new code on iOS 15+, prefer AsyncSequence for simpler async streams.
  • Use AsyncStream/AsyncThrowingStream to wrap callback-based APIs.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func notifications() -> AsyncStream<Notification> {
    AsyncStream { continuation in
        let token = NotificationCenter.default.addObserver(forName: .foo, object: nil, queue: nil) { note in
            continuation.yield(note)
        }
        continuation.onTermination = { _ in
            NotificationCenter.default.removeObserver(token)
        }
    }
}

Concurrency Safety

  • Ensure custom publishers and subjects are Sendable if they cross actors/threads. Consider using actors to guard mutable state in publishers you own.

Testing

  • For Combine: use XCTest with expectations or TestScheduler; assert output sequences and completion.
  • For async bridges: write async tests with for await and verify cancellation by cancelling the parent Task.