When do you use Combine, AsyncSequence or the Observation framework, and when should you not use Combine?
Predict first
How do you read a publisher’s values in async code?
import Combine
var vals: [Int] = []
for await v in [1, 2, 3].publisher.values { vals.append(v) }
print(vals)
- [1, 2, 3]
- []
- It does not compile
Mental model. Combine is a plumbing system with valves and splitters: ideal when many pipes join. async/await is a conversation: ask, wait for the answer, carry on. Most calls are conversations, not plumbing.
Combine is a push-based, operator-rich framework: it models a value changing over time and composes streams declaratively (combineLatest, debounce, switchToLatest, share). It is strongest for several streams combined with timing semantics (form validation from many fields, search-as-you-type, merging sensor feeds) and for existing code and APIs that already expose publishers (NotificationCenter, Timer, URLSession.dataTaskPublisher, @Published). AsyncSequence (with AsyncStream) is pull-based and works with structured concurrency: you consume with for await, cancellation and backpressure come from the task, errors use throws, and there is no subscription object to store. It is the better fit for one request, or a sequential consumer such as an event stream you read in a loop. The Swift Async Algorithms package adds the operators Combine has (debounce, merge, combineLatest, zip), and publisher.values turns a publisher into an async sequence when you need to cross over. Observation (@Observable) solves a different problem: UI state tracking. It tells SwiftUI which views depend on which properties, without a pipeline at all.
When not to use Combine: for a single async result (an async function is simpler and cancellable by structure), for state that only drives SwiftUI (use @Observable), where the team is already on async/await and the pipeline adds a second concurrency model to learn, for deep nesting of flatMaps that read better as sequential awaits, and for cross-platform or server code (Combine is Apple-only; OpenCombine exists but is another dependency). Also be wary of its costs: generic types that are hard to read and slow to compile, memory management through AnyCancellable, ordering surprises across receive(on:) hops, and no first-class support for Swift 6 isolation (closures are not @Sendable-checked the way tasks are, which makes strict concurrency migrations harder). A good policy: keep working Combine where it is, wrap it at boundaries with .values or AsyncStream, write new single-shot work with async/await, and reach for Combine (or Async Algorithms) only when stream composition really is the problem.
The values property bridges a publisher into an AsyncSequence.
| Combine | AsyncSequence / async | Observation | |
|---|---|---|---|
| Model | push, operator pipelines | pull, for await | property tracking |
| Best for | combining timed streams | one result or a sequential stream | driving SwiftUI state |
| Cancellation | AnyCancellable | task cancellation | not needed |
| Backpressure | demand (mostly unlimited) | built in (pull) | not applicable |
Common traps
- Using Combine for a single request. A
FutureordataTaskPublisherchain is heavier and harder to cancel than anasynccall. - Mixing both without a clear boundary. Convert at the edges with
.valuesorAsyncStream, not in the middle.
They might follow up with
- How do you consume a publisher with
for await? Iteratepublisher.values. - What replaces
@Publishedfor SwiftUI state?@Observablemodels.
Related questions
- 11.1.2 What is demand and backpressure in Combine, and how does a custom subscriber control it?
- 11.5.2 How do failures work in Combine, what does Never mean, and how do catch, replaceError, retry and mapError…
- 10.6.2 How do you bridge repeated events, GCD and Combine into async code with AsyncStream?
How do you test Combine pipelines, and what is a TestScheduler… →