Introduction
Modern iOS applications constantly react to changing data. User input, network responses, database updates, notifications, and system events all represent streams of information that applications need to handle efficiently.
The Combine framework provides Apple developers with a reactive programming model for processing asynchronous events over time.
When combined with SwiftUI, Combine enables powerful state-driven applications where views automatically update when underlying data changes.
Although Swift concurrency with async/await is now preferred for many new asynchronous operations, Combine remains important because many Apple frameworks and existing applications rely on publishers and reactive streams.

What Is Combine Framework?
Combine is Apple’s framework for handling asynchronous events by using publishers that emit values and subscribers that receive those values.
Instead of manually calling callbacks whenever data changes, Combine creates a pipeline where data flows automatically through transformations.
| Traditional Callbacks | Combine |
|---|---|
| Manual completion handlers | Reactive pipelines |
| Nested callback code | Composable operators |
| Hard cancellation | Built-in subscriptions |
| Manual state updates | Automatic publishing |
Core Concepts of Combine
Combine is built around three main concepts: publishers, subscribers, and operators.
| Concept | Purpose |
|---|---|
| Publisher | Produces values over time |
| Subscriber | Receives values |
| Operator | Transforms data streams |
Publishers in Combine
A publisher describes a source of values that can arrive asynchronously.
Publishers can represent network responses, user actions, timers, notifications, or custom events.
let publisher =
Just("Hello Combine")
publisher
.sink { value in
print(value)
}
Subscribers
Subscribers consume values emitted by publishers. The most common subscriber in Combine is sink.
publisher
.sink(
receiveCompletion: { result in
print(result)
},
receiveValue: { value in
print(value)
}
)
AnyCancellable and Memory Management
Combine subscriptions must be stored to remain active. AnyCancellable represents a subscription that can be cancelled manually.
class ViewModel {
private var cancellables =
Set<AnyCancellable>()
func subscribe() {
publisher
.sink { value in
print(value)
}
.store(
in: &cancellables
)
}
}
Combine with ObservableObject
ObservableObject is one of the most common ways Combine integrates with SwiftUI. Objects marked as ObservableObject notify views when published properties change.
class UserViewModel:
ObservableObject {
@Published
var username = ""
}
@Published Property Wrapper
@Published automatically creates a publisher for a property. Whenever the value changes, subscribers receive a new value.
class SettingsViewModel:
ObservableObject {
@Published
var isDarkMode = false
}
Using Combine in SwiftUI Views
SwiftUI observes ObservableObject instances and automatically refreshes the UI when published properties change.
struct ContentView:
View {
@StateObject
private var viewModel =
UserViewModel()
var body: some View {
Text(
viewModel.username
)
}
}
Combine Operators
Operators transform publisher output and allow developers to create complex asynchronous workflows.
- ●
map
- ●
filter
- ●
combineLatest
- ●
merge
- ●
debounce
- ●
removeDuplicates
- ●
flatMap
Transforming Data with map Operator
The map operator transforms values emitted by a publisher. It is commonly used to convert API models into UI models or modify data before displaying it.
let numbers =
[1, 2, 3]
.publisher
numbers
.map {
$0 * 2
}
.sink { value in
print(value)
}
Filtering Data with filter
The filter operator allows only values that match a specific condition to continue through the publisher pipeline.
searchResults
.filter {
$0.isActive
}
.sink { item in
print(item)
}
Combining Multiple Publishers
Applications often need to react to multiple data sources at the same time. Combine provides operators for merging and combining publishers.
Publishers
.CombineLatest(
usernamePublisher,
passwordPublisher
)
.map {
username,
password in
return !username.isEmpty
&& !password.isEmpty
}
.sink { valid in
print(valid)
}
Debounce for Search Fields
Debounce is one of the most useful Combine operators in SwiftUI applications. It delays events until the user stops producing new values.
A common example is a search field where API requests should only happen after the user finishes typing.
class SearchViewModel:
ObservableObject {
@Published
var query = ""
private var cancellables =
Set<AnyCancellable>()
init() {
$query
.debounce(
for: .milliseconds(500),
scheduler: DispatchQueue.main
)
.sink { value in
self.search(value)
}
.store(
in: &cancellables
)
}
}
Networking with Combine and URLSession
Combine integrates directly with URLSession, allowing developers to create reactive networking pipelines.
class ApiService {
func fetchUsers()
-> AnyPublisher<[User], Error> {
URLSession.shared
.dataTaskPublisher(
for:
URL(
string:
"https://api.example.com/users"
)!
)
.map {
$0.data
}
.decode(
type:
[User].self,
decoder:
JSONDecoder()
)
.eraseToAnyPublisher()
}
}
Repository Pattern with Combine
Combine works well with the repository pattern because repositories can expose publishers while hiding networking details from ViewModels.
protocol UserRepository {
func users()
-> AnyPublisher<
[User],
Error
>
}
class UserRepositoryImpl:
UserRepository {
private let api:
ApiService
init(
api: ApiService
) {
self.api = api
}
func users()
-> AnyPublisher<
[User],
Error
> {
api.fetchUsers()
}
}
Combine with MVVM Architecture
A common SwiftUI architecture uses Combine inside ViewModels to transform repository data into UI state.
class UsersViewModel:
ObservableObject {
@Published
var users:
[User] = []
private let repository:
UserRepository
private var cancellables =
Set<AnyCancellable>()
init(
repository:
UserRepository
) {
self.repository =
repository
}
func loadUsers() {
repository
.users()
.receive(
on:
DispatchQueue.main
)
.sink(
receiveCompletion:
{ _ in },
receiveValue:
{
users in
self.users =
users
}
)
.store(
in:
&cancellables
)
}
}
Subjects in Combine
Subjects are special publishers that allow developers to manually send values. They are useful for communication between different parts of an application.
- ●
PassthroughSubject
- ●
CurrentValueSubject
PassthroughSubject Example
PassthroughSubject does not store the current value. It only sends new events to active subscribers.
let subject =
PassthroughSubject<String, Never>()
subject
.sink { value in
print(value)
}
subject.send(
"New Message"
)
CurrentValueSubject Example
CurrentValueSubject stores the latest value and immediately sends it to new subscribers.
let subject =
CurrentValueSubject<Int, Never>(
0
)
subject.send(10)
subject
.sink { value in
print(value)
}
Timer Publishers
Combine can create publishers for timers. This is useful for countdowns, clocks, progress indicators, and periodic updates.
Timer
.publish(
every:
1,
on:
.main,
in:
.common
)
.autoconnect()
.sink { date in
print(date)
}
NotificationCenter with Combine
Combine provides publishers for system notifications, allowing applications to react to events without traditional observers.
NotificationCenter
.default
.publisher(
for:
UIApplication
.didEnterBackgroundNotification
)
.sink { _ in
print(
"App entered background"
)
}
Combine vs Async/Await
Swift concurrency introduced async/await, which provides a simpler approach for handling many asynchronous operations. However, Combine remains useful for continuous data streams and reactive pipelines.
| Combine | Async/Await |
|---|---|
| Reactive streams | One-time async operations |
| Multiple emitted values | Single returned value |
| Powerful operators | Simpler syntax |
| Complex event processing | API calls and tasks |
For example, downloading a user profile usually fits async/await, while observing a search field, database updates, or application events often fits Combine.
Using Combine and Async/Await Together
Modern Swift applications can use both approaches together. Combine can handle continuous streams while async/await handles individual asynchronous operations.
func loadProfile()
async throws -> User {
let user =
try await api
.fetchUser()
return user
}
A ViewModel can expose the final state using @Published while internally using async/await for data loading.
Testing Combine Pipelines
Combine pipelines should be tested to ensure publishers emit expected values, handle failures correctly, and complete properly.
func testPublisher() {
let expectation =
XCTestExpectation()
publisher
.sink { value in
XCTAssertEqual(
value,
expected
)
expectation
.fulfill()
}
}
Testing ViewModels with Combine
ViewModels using Combine should be tested by verifying state changes after receiving publisher values.
- ●
Initial state
- ●
Loading state
- ●
Success state
- ●
Error handling
- ●
Cancellation behavior
Combine Memory Management
Combine subscriptions create references between publishers and subscribers. Incorrect handling can lead to memory leaks.
- ●
Store AnyCancellable references
- ●
Use weak self inside closures
- ●
Cancel subscriptions when no longer needed
- ●
Avoid strong reference cycles
publisher
.sink { [weak self] value in
self?
.update(value)
}
.store(
in:
&cancellables
)
Combine Best Practices
- ●
Keep publishers inside service and repository layers
- ●
Expose simple state to SwiftUI views
- ●
Avoid large Combine chains
- ●
Use meaningful operators
- ●
Handle errors explicitly
- ●
Cancel unused subscriptions
💡 Architecture Tip
SwiftUI views should not contain Combine logic. Keep reactive pipelines inside ViewModels or dedicated services.
Common Combine Mistakes
- ●
Creating subscriptions inside Views
- ●
Forgetting to store AnyCancellable
- ●
Ignoring completion events
- ●
Using Combine when async/await is simpler
- ●
Creating complicated publisher chains
- ●
Not handling errors
Production SwiftUI Combine Architecture

App/
Views/
ContentView.swift
ViewModels/
UserViewModel.swift
Repositories/
UserRepository.swift
Services/
ApiService.swift
Models/
User.swift
When Should You Use Combine?
Combine is still valuable in many SwiftUI applications, especially when dealing with continuous streams of data.
- ●
Real-time updates
- ●
Search suggestions
- ●
Form validation
- ●
Notifications
- ●
Timers
- ●
Reactive UI state
- ●
Existing Apple framework integrations
When Should You Prefer Async/Await?
- ●
REST API requests
- ●
File operations
- ●
Single database operations
- ●
Background tasks
- ●
Sequential asynchronous workflows
Frequently Asked Questions
Is Combine still relevant with async/await?
Should every SwiftUI app use Combine?
Is @Published part of Combine?
Can Combine work with SwiftUI?
Should I learn Combine before async/await?
Conclusion
The Combine framework remains an important technology in the Swift ecosystem. Together with SwiftUI, it provides a powerful reactive programming model for building responsive applications. Understanding publishers, subscribers, operators, and state management allows developers to create cleaner architectures and handle complex asynchronous workflows effectively.
