A Publisher emits a sequence of values (and possibly a completion or failure) over time; a Subscriber receives them, most commonly via sink (a simple closure-based subscription) or assign (directly writing emitted values to a property). @Published, applied to a property, automatically creates a Publisher that emits every time the property changes — the mechanism SwiftUI's data-binding relies on. Since async/await matured into Swift's standard tool for asynchronous work, Apple's own guidance and much of the community have shifted toward preferring it for single-result async operations, reserving Combine specifically for genuine streams — ongoing sequences of values like user text input or repeated property changes — where Combine's operators (map, filter, debounce, and others) still offer real value async/await alone doesn't directly provide.
@Published: turning a property into a stream
Subscribing to $name's sink fires immediately with name's CURRENT value at the moment of subscription (the projected publisher behaves like a CurrentValueSubject) — then again on every subsequent assignment. Apple's own documentation example shows this exactly: subscribing when temperature is already 20 prints "Temperature now: 20.0" immediately, before the later assignment to 25 prints again.
import Combine
class UserViewModel: ObservableObject {
@Published var name: String = ""
}
let viewModel = UserViewModel()
var cancellables = Set<AnyCancellable>()
viewModel.$name.sink { newValue in
print("Name changed to: \(newValue)")
}.store(in: &cancellables)
// fires immediately on subscription with the CURRENT value: prints "Name changed to: " (name is still "")
viewModel.name = "Alice" // prints "Name changed to: Alice"
viewModel.name = "Bob" // prints "Name changed to: Bob"Operators: transforming a stream
map and filter behave conceptually just like their Array counterparts, but operate on VALUES ARRIVING OVER TIME rather than a fixed, already-complete collection.
let numbers = PassthroughSubject<Int, Never>()
numbers
.filter { $0 % 2 == 0 }
.map { $0 * 10 }
.sink { print($0) }
.store(in: &cancellables)
numbers.send(3) // filtered out — odd
numbers.send(4) // 40 — even, passes the filter, then multiplied by 10 by map (not "doubled" — the closure is { $0 * 10 })When to reach for Combine vs. async/await
A single network call fetching one result is now usually simpler as an async function; Combine's real, ongoing strength is genuine ongoing streams — debouncing rapid text-field input, or reacting continuously to a property that changes many times over an app's lifetime.