Combine is Apple’s reactive framework: values flow through publishers, are transformed by operators, and delivered to subscribers. Learn the model and where it fits alongside async/await.
Combine models asynchronous events as streams: a publisher emits values over time, operators transform and combine them, and a subscriber receives the results. It is the declarative, reactive counterpart to imperative callbacks — the same family as RxSwift and Android’s Flow. You reach for it when you have streams of events to react to, like text-field changes or timers.
Combine = values flowing through a pipeline:
PUBLISHER emits values over time (a text field, a timer, a network call)
| OPERATORS transform the stream (map, filter, debounce, combineLatest)
v
SUBSCRIBER receives the results (sink, assign, or a SwiftUI view)
It's the reactive counterpart to callbacks — declarative streams you compose.
Same family as RxSwift (iOS) and Flow (Android). Use it for STREAMS of events;
use async/await for one-shot async work.You build a Combine pipeline by chaining operators onto a publisher and ending with sink to consume values. Operators like map and filter mirror the collection functions but run over time as values arrive. Cancellables track active subscriptions; storing them keeps the pipeline alive and tears it down when the owner is deallocated.
import Combine
var cancellables = Set<AnyCancellable>()
let subject = PassthroughSubject<Int, Never>() // a publisher you push values into
subject
.filter { $0 % 2 == 0 } // operators transform the stream over time
.map { $0 * 10 }
.sink { value in // subscriber consumes the results
print(value) // 20, 40
}
.store(in: &cancellables) // keep the subscription alive
subject.send(1); subject.send(2); subject.send(4)
// store cancellables on the owner; when it deallocates, the pipeline tears down.Combine shines for event streams that need timing control. A classic example is search-as-you-type: debounce waits for the user to stop typing, removeDuplicates skips redundant queries, and the pipeline drives the request. Expressing this declaratively is far cleaner than manual timers and flags, and it is where Combine earns its place in an otherwise async/await codebase.
@Observable
final class SearchViewModel {
var query = ""
var results: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
// react to query changes, but only after the user pauses typing
_query.publisher // (illustrative) a publisher of query
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates() // skip identical consecutive queries
.sink { [weak self] text in self?.search(text) }
.store(in: &cancellables)
}
func search(_ text: String) { /* fetch + assign results */ }
}
// debounce + removeDuplicates = clean search-as-you-type. Declarative timing
// that would be messy with manual timers + flags.Combine and async/await overlap, so pick by shape: use async/await for one-shot asynchronous work (fetch this, await the result), and Combine for ongoing streams of events over time (a value that keeps changing). Many modern apps lean on async/await for networking and reach for Combine only where a reactive stream is the natural fit. Knowing which to use keeps code simple.
When to use which:
async / await ONE-SHOT async work: "fetch this, await the result"
-> networking, load-on-appear, sequential async steps
the default for most async code today.
Combine STREAMS of values over time that you react to:
-> text-field debounce, timers, combining multiple sources,
driving pipelines that keep emitting
Rule of thumb: reach for async/await first; use Combine where a reactive
STREAM is genuinely the right shape. They coexist in the same app.