What is Combine? Explain publishers, subscribers and operators.
MediumCombineConcurrency 2 min read
TL;DR
Combine (iOS 13) handles values over time: text input, network results, notifications, timers.
- A Publisher emits values, then finishes or fails.
- Operators transform them (
map,filter,debounce,combineLatest). - A Subscriber receives them (
sink,assign). - The subscription lives as long as you keep the
AnyCancellable.
Today, new code often uses async/await, AsyncSequence and Observation. But Combine is still common in existing apps, so interviewers still ask about it.
A first example
import Combine
let cancellable = [1, 2, 3].publisher
.map { $0 * 10 }
.filter { $0 > 10 }
.sink { value in
print(value) // 20, 30
}Subjects: publishers you push into
let taps = PassthroughSubject<Void, Never>() // no current value
let username = CurrentValueSubject<String, Never>("") // has a current value
taps.send()
username.send("ana")
print(username.value) // "ana"@Published gives a property a publisher ($property):
final class SearchViewModel: ObservableObject {
@Published var query = ""
@Published private(set) var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init(search: @escaping (String) -> AnyPublisher<[String], Never>) {
$query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.filter { $0.count >= 2 }
.map { search($0) }
.switchToLatest() // cancel the old search
.receive(on: DispatchQueue.main)
.assign(to: &$results) // no cancellable needed
}
}This is the classic "search as you type" interview example.
Memory: AnyCancellable
var cancellables = Set<AnyCancellable>()
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.sink { [weak self] _ in self?.refresh() }
.store(in: &cancellables)- If you do not store the cancellable, the subscription ends right away.
sinkclosures that captureselfstrongly create retain cycles. Use[weak self].assign(to: \.prop, on: self)keepsselfstrongly. That is a cycle. Preferassign(to: &$prop).
Important operators
| Operator | What it does |
|---|---|
map, filter, compactMap | Like on arrays |
debounce | Wait for a pause in values (typing) |
throttle | At most one value per interval |
removeDuplicates | Skip equal values in a row |
combineLatest | Latest value from each of several publishers |
merge | Mix values from publishers of the same type |
flatMap | Start a new publisher for each value |
switchToLatest | Like flatMap, but cancels the previous inner publisher |
catch, replaceError | Handle errors |
receive(on:) | Deliver values on a scheduler, like the main queue |
Form validation with combineLatest:
Publishers.CombineLatest($email, $password)
.map { email, password in email.contains("@") && password.count >= 8 }
.assign(to: &$isFormValid)Errors
A publisher can fail once, and then it is finished.
If an error reaches the end of a flatMap chain, the whole stream stops. Catch errors inside the flatMap:
.flatMap { query in
api.search(query)
.catch { _ in Just([]) } // the outer stream keeps going
}Networking
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [User].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { print($0) }, receiveValue: { users in print(users) })
.store(in: &cancellables)Combine and async/await
for await value in $query.values { // publisher -> AsyncSequence
print(value)
}| Combine | Modern Swift |
|---|---|
@Published + ObservableObject | @Observable |
Future / one-shot publisher | async function |
| Publisher stream | AsyncSequence / AsyncStream |
debounce, combineLatest on async sequences | swift-async-algorithms package |
Common follow-up questions
- Hot vs cold publishers? Cold ones start work for each subscriber (
dataTaskPublisher). Hot ones emit whether or not anyone listens (subjects, notifications). Useshare()to share one upstream. - What is back pressure? A subscriber asks for a number of values (
Demand).sinkasks for unlimited. - Why
eraseToAnyPublisher()? It hides the long nested generic type behindAnyPublisher<Output, Failure>. - Is Combine deprecated? No, but Apple's new APIs focus on Swift concurrency and Observation.