How does @Published work, and why does a subscriber see the old value inside its sink?

11.3.2 · L3 · Interview

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 retains self and creates a cycle; use assign(to: &$prop).

They might follow up with

  • Why does a $property sink fire immediately? It replays the current value on subscription, like a current-value subject.
  • What replaces @Published with Observation? @Observable classes, 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?

← What is the difference between PassthroughSubject and…