How does @Published work, and why does a subscriber see the old value inside its sink?
Predict first
What does the subscriber see for the property and the argument?
import Combine
final class VM { @Published var n = 0 }
var bag = Set<AnyCancellable>()
let vm = VM()
var seen: [String] = []
vm.$n.dropFirst().sink { v in seen.append("arg \(v) prop \(vm.n)") }.store(in: &bag)
vm.n = 5
print(seen)
- ["arg 5 prop 5"]
- ["arg 5 prop 0"]
- ["arg 0 prop 5"]
Mental model. @Published announces “I am about to change to 5” before it changes, so anyone who checks the property in that moment still sees the old value.
@Published is a property wrapper that stores the value and exposes a publisher through the projected value: $name is a Published<T>.Publisher (never fails). Subscribing to it behaves like a current-value subject: the subscriber receives the current value immediately, then each change. Only a class can use it for objectWillChange synergy: an ObservableObject gets a synthesised objectWillChange that fires whenever any @Published property is about to change, which is what SwiftUI’s @ObservedObject and @StateObject listen to. Because the wrapper uses willSet semantics, the publisher emits before the property is updated: inside a sink on $n, reading vm.n gives the old value, while the closure’s argument is the new one. Always use the argument, not the property.
The practical rules: use the projected publisher with operators for derived state ($query.debounce(…).sink), subscribe with dropFirst() when you only want changes, and bind a pipeline’s output to a @Published property with assign(to: &$property) (the form taking inout Published.Publisher), which needs no AnyCancellable and cannot create a retain cycle (unlike assign(to:on:), which retains its target). A @Published property’s publisher is per instance and delivers on the thread that set the value, so set it from the main thread for UI. With the Observation framework (@Observable) you no longer need @Published for SwiftUI at all; keep it for code that genuinely wants a publisher.
The publisher fires on willSet, so the new value is in the argument while the property still holds the old value.
Common traps
- Reading the property inside its own
sink. It still holds the old value; use the closure’s argument. - Using
assign(to:on: self). It retainsselfand creates a cycle; useassign(to: &$prop).
They might follow up with
- Why does a
$propertysink fire immediately? It replays the current value on subscription, like a current-value subject. - What replaces
@Publishedwith Observation?@Observableclasses, tracked by the views that read them.
Related questions
- 11.3.1 What is the difference between PassthroughSubject and CurrentValueSubject, and when do you use a subject at…
- 11.5.1 What is AnyCancellable, how long does a subscription live, and how do you manage it in a view model?
- 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?