What happens when you send to a subject after it has completed, and what does a late subscriber to a finished subject receive?
Predict first
What does each subscriber see, and what happens after completion?
import Combine
var bag = Set<AnyCancellable>()
let p = PassthroughSubject<Int, Never>()
p.send(0)
p.sink { print("passthrough", $0) }.store(in: &bag)
p.send(1)
let c = CurrentValueSubject<Int, Never>(10)
c.send(11)
c.sink { print("current", $0) }.store(in: &bag)
c.send(12)
p.send(completion: .finished)
p.send(2)
c.send(completion: .finished)
c.send(13)
print("value after finish:", c.value)
c.sink(receiveCompletion: { print("late subscriber completion", $0) },
receiveValue: { print("late subscriber value", $0) }).store(in: &bag)
- passthrough 1 / current 11 / current 12 / value after finish: 12 / late subscriber completion finished
- passthrough 0 / passthrough 1 / current 10 / current 11 / current 12 / value after finish: 13
- passthrough 1 / current 12 / value after finish: 13 / late subscriber value 13
Mental model. A PassthroughSubject is a live radio broadcast: tune in late and you miss what was said. A CurrentValueSubject is a notice board: you always read the latest notice when you walk up.
A PassthroughSubject has no memory: a subscriber receives only the values sent after it subscribed. The 0 sent before subscribing is lost (the log starts at “passthrough 1”). A CurrentValueSubject holds one current value and replays it to every new subscriber immediately: subscribing after send(11) prints “current 11” at once and then follows with “current 12”. It also exposes the latest value as .value, readable synchronously (the property is the same state that send updates).
After send(completion:) a subject is finished: later send calls are silently ignored (the 2 and 13 never arrive, and value stays 12), and a new subscriber gets the completion straight away (“late subscriber completion finished”) and, for a CurrentValueSubject, no value, because after finishing it replays nothing. Use CurrentValueSubject for state (“the current user”, “the selected filter”) where a new observer needs the latest value, and PassthroughSubject for events (“button tapped”) where replaying an old one would be wrong. Neither buffers beyond that, so for “replay the last N” you need your own buffer. Expose subjects as AnyPublisher from your API so outsiders cannot call send.
Passthrough has no memory, CurrentValue replays one value, and a finished subject ignores sends and completes late subscribers.
Common traps
- Sending to a subject after it finished. It is silently ignored, so nothing says why the UI stopped updating.
- Using a
PassthroughSubjectfor state. A screen that appears later never learns the current value.
They might follow up with
- Is mutating
currentValueSubject.valuethe same assend? Yes; settingvaluepublishes. - How is
@Publishedrelated? It behaves like aCurrentValueSubjectexposed as$property(Q308).
Related questions
- 11.8.1 When does a Future run its work, what do two subscribers get, and what does Deferred change?
- 11.8.3 What does share() do for several subscribers, and what happens to a subscriber who arrives after the shared…
- 11.3.1 What is the difference between PassthroughSubject and CurrentValueSubject, and when do you use a subject at…
- 11.3.2 How does @Published work, and why does a subscriber see the old value inside its sink?
← When does a Future run its work, what do two subscribers get, and… What does share() do for several subscribers, and what happens to a… →