What is AnyCancellable, how long does a subscription live, and how do you manage it in a view model?
Predict first
A subscriber is held in a variable, then released. What does it receive?
import Combine
var cc: [Int] = []
let sub = PassthroughSubject<Int, Never>()
var c: AnyCancellable? = sub.sink { cc.append($0) }
sub.send(1)
c = nil
sub.send(2)
print(cc)
- [1, 2]
- [1]
- []
Predict first
The result of sink is ignored. What happens?
import Combine
var got: [Int] = []
let sub = PassthroughSubject<Int, Never>()
_ = sub.sink { got.append($0) }
sub.send(1)
print(got)
- [1]
- []
Mental model. The AnyCancellable is the ticket stub for a running tap. If you throw the stub away, the tap is turned off straight away.
sink and assign return an AnyCancellable. It is the handle that keeps the subscription alive: while it exists the subscription is active, and when it is deallocated it cancels the subscription automatically. That is the most common Combine bug in both directions: if you drop the return value (or let it go out of scope) the pipeline is cancelled at once and you see no values; if you keep it forever, the pipeline outlives what it should. Store it: in a property (var cancellable: AnyCancellable?), or collect many in a Set<AnyCancellable> using .store(in: &cancellables), owned by the object whose lifetime matches the work, so that releasing the object cancels everything. You can also call cancel() explicitly, or set the property to nil, to stop a pipeline early (after which no more values arrive, not even a completion). Cancelling propagates upstream, so a cancelled request publisher cancels the underlying network task.
Common patterns and traps. Retain cycles: a sink closure that captures self strongly while self stores the cancellable is a cycle; capture [weak self] (the cycle details are in the memory topic). Replace on re-subscribe: when starting a new request, assigning a new cancellable to the same property cancels the old one, which is a cheap “latest only”. Lifetime of the subscription versus the publisher: a publisher completing ends the subscription, but the cancellable object may remain; that is harmless. In SwiftUI, prefer onReceive and let the view own the lifetime, and in async code the equivalent is a Task you cancel in deinit or tie to a view with .task. When a pipeline “does nothing”, check first that the cancellable is being stored.
Releasing the cancellable cancels the subscription, so the second value is never delivered.
The temporary cancellable is released immediately, cancelling the subscription before the value is sent.
Common traps
- Ignoring the result of
sink. The cancellable is released at once and the pipeline is cancelled before any value arrives. - Capturing
selfstrongly in a stored sink. It creates a cycle with the stored cancellable; use[weak self].
They might follow up with
- What happens if you set the stored cancellable to
nil? The subscription is cancelled, with no further values. - How do you cancel several pipelines together? Store them in one
Set<AnyCancellable>and release orremoveAll()it.
Related questions
- 11.2.2 How do flatMap and switchToLatest differ, and when do you use each?
- 11.3.2 How does @Published work, and why does a subscriber see the old value inside its sink?
- 3.3.5 How do Combine subscriptions create cycles, and why is assign(to:on:) a trap?
How do failures work in Combine, what does Never mean, and how do… →