name: combine description: Use when writing, reviewing, or migrating Combine publishers, subscribers, subjects, operator chains, or async bridges.
Combine
Review and write Combine code for correctness, memory management, and async/await interop. Diagnose silent pipeline failures. Guide migration decisions without rewriting working code.
Responsibility
Owns: Publisher/Subscriber lifecycle, operator chains, Subjects, @Published, AnyCancellable management, Schedulers, error handling in pipelines, Combine-to-async bridging, cold vs hot publishers, share/multicast.
Does NOT own: async/await patterns (swift-concurrency), @Observable (swift-concurrency), Timer.publish lifecycle, SwiftUI view architecture, networking layer design.
Core Principles
- Combine is mature, not dead -- Apple has not deprecated it. Do not rewrite working pipelines. Bridge at boundaries.
- AnyCancellable lifecycle is the #1 bug source -- Every sink needs
[weak self]andstore(in:). Everyassign(to:on:)withselfis a retain cycle. - Completion kills the pipeline permanently -- Once a publisher sends
.finishedor.failure, all subsequentsend()calls are silently ignored. This is the most common cause of "my pipeline stopped working." - Thread safety is your problem --
@Publishedis not thread-safe. Setting it from a background thread crashes SwiftUI. Usereceive(on: RunLoop.main)or@MainActor. - Error handling position matters --
replaceErrorafterflatMapkills the outer pipeline on the first inner error. Handle errors insideflatMap. - Cold publishers duplicate work --
URLSession.dataTaskPublisherfires a new request per subscriber. Useshare()when multiple subscribers consume one expensive publisher.
Decision Tree: Combine vs async/await vs AsyncAlgorithms
Is it a one-shot operation (network call, file read)?
Yes --> async/await
Does it need time-based operators (debounce, throttle)?
Yes, new code --> AsyncAlgorithms (.debounce, .throttle)
Yes, existing Combine --> keep Combine
Are you combining multiple ongoing streams?
Yes, new code --> AsyncAlgorithms (merge, combineLatest, zip)
Yes, existing Combine --> keep Combine
Is it existing Combine code that works?
Yes --> keep it, bridge with .values at boundaries
Is this new code?
Yes --> async/await + @Observable
Operator Mapping: Combine to AsyncAlgorithms
| Combine | AsyncAlgorithms | Notes |
|---|---|---|
debounce(for:) |
.debounce(for:) |
Emits after silence window |
throttle(for:latest:) |
.throttle(for:latest:) |
Rate-limits emissions |
merge(with:) |
merge(_:_:) |
Free function, 2-3 inputs |
combineLatest(_:) |
combineLatest(_:_:) |
Free function, emits tuple |
zip(_:) |
zip(_:_:) |
Free function, pairs 1:1 |
removeDuplicates() |
.removeDuplicates() |
Consecutive equal elements |
prepend(_:) |
chain(_:_:) |
Concatenates sequences |
collect(.byCount(n)) |
.chunks(ofCount:) |
Fixed-size batches |
collect(.byTime(...)) |
.chunked(by: .repeating(every:)) |
Time-windowed batching |
buffer(size:) |
.buffer(policy:) |
Bounded buffer with back-pressure |
Full operator reference: references/combine-patterns.md
Full migration guide: references/migration-to-async.md
Diagnostic Checklist: Silent Pipeline Failure
When a pipeline stops producing values with no crash or error:
- Is the
AnyCancellablestill stored? (not deallocated, Set not cleared) - Did anything upstream send
.finishedor.failure? - Is there a
tryMapor throwing operator without error handling downstream? - Was
switchToLatestused where the outer publisher completed? - Is
assign(to:on:)used withselfas target? (retain cycle, deinit never called)
Quick Reference
AnyCancellable Rules
| Pattern | Result |
|---|---|
sink { } without store(in:) |
Pipeline cancelled immediately |
sink { self.x } without [weak self] |
Retain cycle |
assign(to:on: self) |
Retain cycle -- use assign(to: &$prop) |
| Re-subscribing without clearing set | Old subscriptions accumulate |
Subject Selection
| Need | Use |
|---|---|
| Events (taps, notifications) | PassthroughSubject |
| State with current value | CurrentValueSubject |
| SwiftUI-bound property | @Published |
| New code, event stream | AsyncStream instead |
| New code, observable state | @Observable instead |
Core Instructions
- Diagnose before rebuilding. Silent failures have a cause -- find it.
- Use
[weak self]in everysinkclosure that capturesself. - Use
assign(to: &$prop)instead ofassign(to:on: self). - Use
receive(on: RunLoop.main)or@MainActorbefore UI updates. - Handle errors inside
flatMap, not downstream of it. - Use
share()only when multiple subscribers consume one expensive publisher. - Bridge with
.values(Combine to async) orFuture(async to Combine). Do not rewrite working pipelines.
References
references/combine-patterns.md-- Publisher/Subscriber model, operators, Subjects, @Published, error handling, Schedulers, memory management, custom publishers.references/migration-to-async.md-- Operator-to-AsyncAlgorithms mapping, Publisher.values bridging, replacing sink/Published/Subject, before/after migration patterns, when Combine is still the right choice.