What is the difference between PassthroughSubject and CurrentValueSubject, and when do you use a subject at all?
Predict first
Both subjects are sent 1 before anyone subscribes and 2 afterwards. What does each subscriber see?
import Combine
var bag = Set<AnyCancellable>()
let ps = PassthroughSubject<Int, Never>()
let cs = CurrentValueSubject<Int, Never>(0)
ps.send(1); cs.send(1)
var lp: [Int] = [], lc: [Int] = []
ps.sink { lp.append($0) }.store(in: &bag)
cs.sink { lc.append($0) }.store(in: &bag)
ps.send(2); cs.send(2)
print(lp, lc, cs.value)
- [2] [1, 2] 2
- [1, 2] [1, 2] 2
- [2] [2] 2
Mental model. A passthrough subject is a loudspeaker: you hear it only if you are in the room when it speaks. A current-value subject is a scoreboard: anyone who walks in sees the current score first.
A subject is both a publisher and something you can send values and completions into. PassthroughSubject<Output, Failure> has no state: it forwards values to current subscribers and forgets them, so a subscriber that attaches later sees only what is sent after it subscribes. Use it for events (a button was tapped, a message arrived, “show the alert”). CurrentValueSubject holds a current value (readable and writable through .value) and replays it to every new subscriber immediately on subscription, then forwards updates. Use it for state (the current user, a connectivity status). A new subscriber to a current-value subject gets one value right away, which is why a sink on it fires immediately, and dropFirst() is the usual way to ignore that initial value.
Use subjects at the boundary between imperative code and pipelines: a delegate callback or a view’s action sends into a subject, and the rest of the code subscribes to subject.eraseToAnyPublisher(). Keep the subject private and expose only the erased publisher, so outsiders cannot push values or complete it. Do not use a subject where an existing publisher or a plain operator does the job: a subject is mutable shared state and an easy way to recreate callback spaghetti. Remember that send delivers on the calling thread and synchronously, and that a subject which receives .finished or a failure is done forever; a current-value subject’s .value is not synchronised, so it needs the same thread discipline as any property. With Swift concurrency, an AsyncStream’s continuation plays the role of a passthrough subject.
The passthrough subject forgot the first value. The current-value subject replayed its current value (1) on subscription, then forwarded 2.
Common traps
- Exposing the subject itself. Outside code can send values or complete it; expose an erased publisher.
- Forgetting the immediate first value of a current-value subject. A
sinkfires on subscription; usedropFirst()if you only want changes.
They might follow up with
- Which for “the user tapped Save”?
PassthroughSubject, an event with no state. - Which for “the current login state”?
CurrentValueSubject, because new subscribers need the current value.
Related questions
- 11.3.2 How does @Published work, and why does a subscriber see the old value inside its sink?
- 11.1.1 What are Publisher, Subscriber, Subscription and Subscriber.Input/Failure, and when does a pipeline actually…
- 11.6.1 When do you use Combine, AsyncSequence or the Observation framework, and when should you not use Combine?
How does @Published work, and why does a subscriber see the old value… →