Working with Combine Framework
1. Understanding Publishers and Subscribers
The Combine framework in iOS Swift provides a declarative approach to handling asynchronous events and data streams. At its core, it revolves around Publisher and Subscriber protocols.
| Component | Description | Key Methods/Properties |
|---|---|---|
| Publisher | Represents a stream of values that can be subscribed to. | subscribe(_:), handleEvents |
| Subscriber | Receives values from a Publisher. | receive(subscription:), receive(_:) |
Example: Basic Publisher and Subscriber Setup
import Combine
// Create a simple publisher
let publisher = PassthroughSubject<String, Never>()
// Create a subscriber
let subscriber = AnySubscriber<String, Never>(receiveCompletion: { completion in
print("Completed with: " + String((describing: completion))
}, receiveValue: { value in
print("Received: " + value)
})
// Subscribe the subscriber to the publisher
publisher.subscribe(subscriber)
// Send a value
publisher.send("Hello, Combine!")
Note: Publishers and Subscribers form the foundation of Combine's reactive programming model, enabling developers to chain operations and react to data changes efficiently.
2. Creating Publishers
Example: Creating a simple publisher from a value
import Combine
let publisher = Just("Hello, World!")
.print("Just Publisher")
// Subscribing to the publisher
publisher
.sink(
receiveCompletion: { completion in
print("Completed with: ", completion)
},
receiveValue: { value in
print("Received value: ", value)
}
)
| Method | Description | Return Type |
|---|---|---|
| Just<Output> | Creates a publisher that emits a single value and finishes | Publisher |
| PassthroughSubject<Output> | Creates a subject that can emit values to subscribers | Subject |
| CurrentValueSubject<Output> | Creates a subject that holds and emits the current value | Subject |
| Future<Output, Failure> | Creates a publisher that completes with a single value or failure | Publisher |
Note:
Just is useful for testing or providing default values in a stream.
Warning:
PassthroughSubject does not hold a value, so subscribers must subscribe before values are emitted to receive them.
3. Using CurrentValueSubject
| Property | Value | Description |
|---|---|---|
| CurrentValueSubject<Output, Failure> | CurrentValueSubject<String, Never> |
Initializes a subject with a current value |
| send(_ value: Output) | subject.send("New Value") |
Sends a new value to all subscribers |
| value | subject.value |
Gets or sets the current value of the subject |
Example: Creating and Using CurrentValueSubject
import Combine
let subject = CurrentValueSubject<String, Never>("Initial Value")
// Subscribe to the subject
let subscription = subject.sink { value in
print("Received: \\(value)")
}
// Send a new value
subject.send("Updated Value")
// Change the current value
subject.value = "New Current Value"
Note:
CurrentValueSubject is useful for representing state that can be observed and updated.4. Subscribing with sink and assign
Example: Using sink to handle values and completion
import Combine
let subject = PassthroughSubject<String, Never>()
let cancellable = subject
.sink(
receiveValue: { value in
print("Received: \\(value)")
},
receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed successfully")
case .failure(let error):
print("Error: \\(error)")
}
}
)
subject.send("Hello")
subject.send("World")
subject.send(completion: .finished)
| Method | Description | Parameters | Return Type |
|---|---|---|---|
| sink(receiveValue:) | Subscribes to a publisher and handles received values | receiveValue: (Value) -> Void |
Cancellable |
| sink(receiveValue:receiveCompletion:) | Subscribes to a publisher and handles values and completion | receiveValue: (Value) -> Void, receiveCompletion: (Subscribers.Completion<Failure>) -> Void |
Cancellable |
Note: The
sink method is a terminal operator that starts the subscription and keeps it alive until the publisher finishes or is cancelled.
Example: Using assign to update a property
import Combine
class ViewModel {
@Published var title = ""
}
let viewModel = ViewModel()
let subject = PassthroughSubject<String, Never>()
let cancellable = subject
.assign(
to: \.title,
on: viewModel
)
subject.send("New Title")
print(viewModel.title) // Output: "New Title"
| Method | Description | Parameters | Return Type |
|---|---|---|---|
| assign(to:) | Assigns received values to a property of an object | to: WritableKeyPath<Object, Value>, on: Object |
Cancellable |
Note: The
assign method is a terminal operator that assigns values to a property and keeps the subscription alive.
5. Using Operators
Example: Transforming Values with map
import Combine
let numbers = [1, 2, 3, 4, 5].publisher
let doubled = numbers.map { $0 * 2 }
doubled.sink(
receiveCompletion: { print("Completed") },
receiveValue: { value in
print("Received: \\(value)")
}
)
// Output:
// Received: 2
// Received: 4
// Received: 6
// Received: 8
// Received: 10
// Completed
| Operator | Description | Example |
|---|---|---|
| map | Transforms each element in the stream | publisher.map { $0 * 2 } |
| filter | Filters elements based on a condition | publisher.filter { $0 > 5 } |
| compactMap | Transforms and filters out nil values | publisher.compactMap { $0?.description } |
| flatMap | Flattens publishers from elements | publisher.flatMap { $0.publisher } |
| scan | Accumulates values over time | publisher.scan(0) { $0 + $1 } |
| removeDuplicates | Removes consecutive duplicate values | publisher.removeDuplicates() |
| first | Emits only the first value | publisher.first() |
| last | Emits only the last value | publisher.last() |
Note: Operators can be chained to create complex data transformations.
Example: Combining Publishers with merge
import Combine
let publisher1 = ["A", "B"].publisher
let publisher2 = ["C", "D"].publisher
let merged = publisher1.merge(with: publisher2)
merged.sink(
receiveCompletion: { print("Completed") },
receiveValue: { value in
print("Received: \\(value)")
}
)
// Output:
// Received: A
// Received: B
// Received: C
// Received: D
// Completed
Example: Combining Publishers with combineLatest
import Combine
let publisher1 = [1, 2].publisher
let publisher2 = ["X", "Y"].publisher
let combined = publisher1.combineLatest(publisher2)
combined.sink(
receiveCompletion: { print("Completed") },
receiveValue: { value in
print("Received: \\(value)")
}
)
// Output:
// Received: (1, "X")
// Received: (2, "X")
// Received: (2, "Y")
// Completed
6. Combining Publishers
Example: Merging Publishers
import Combine
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let mergedPublisher = publisher1.merge(with: publisher2)
mergedPublisher
.sink { value in
print("Merged value: \\(value)")
}
.store(in: &bag)
publisher1.send(1)
publisher2.send(2)
publisher1.send(3)
| Method | Description | Return Type |
|---|---|---|
| merge(with:) | Merges elements from multiple publishers into a single stream | Publisher |
| combineLatest(with:) | Combines the latest values from two publishers | Publisher |
| zip(with:) | Zips values from two publishers into tuples | Publisher |
| switchToLatest() | Switches to the latest inner publisher | Publisher |
Note:
merge(with:) does not preserve order from individual publishers.Example: Combining Latest Values
import Combine
let publisher1 = PassthroughSubject<String, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let combinedPublisher = publisher1
.combineLatest(with: publisher2)
.map { string, number in
return "\\(string) - \\(number)"
}
combinedPublisher
.sink { value in
print("Combined: \\(value)")
}
.store(in: &bag)
publisher1.send("Hello")
publisher2.send(42)
7. Handling Errors in Combine
| Error Type | Description | Handling Strategy |
|---|---|---|
| Error | Generic Swift error type | Use catch operator to handle and transform errors |
| URLError | Errors specific to URL requests in Combine | Use catch with URLError to handle network errors |
| DecodingError | Errors during data decoding in Combine | Use catch with DecodingError to handle JSON or data parsing errors |
Example: Catching Errors in Combine
import Combine
let url = URL(string: "https://api.example.com/data")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [String: Any].self, decoder: JSONDecoder())
.mapError { error in
if let urlError = error as? URLError {
return "Network Error: \(urlError.localizedDescription)"
} else if let decodingError = error as? DecodingError {
return "Decoding Error: \(decodingError.localizedDescription)"
} else {
return "Unknown Error: \(error.localizedDescription)"
}
}
.catch { error in
Just(["error": error.localizedDescription])
.setFailureType(to: Error.self)
}
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed successfully")
case .failure(let error):
print("Failed with error: \(error)")
}
},
receiveValue: { data in
print("Received data: \(data)")
}
)
Note: The
catch operator allows you to transform errors into values or handle them gracefully, ensuring your Combine pipeline can continue processing even when errors occur.
8. Managing Subscriptions
Example: Basic Subscription Management
import Combine
class MyViewModel: ObservableObject {
@Published var data: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
// Subscribe to a publisher
$data
.sink { value in
print("Received: \($0)")
}
.store(in: &cancellables)
}
}
| Method | Description | Usage |
|---|---|---|
| store(in:) | Stores the subscription in a collection to manage its lifetime | cancellable.store(in: &cancellables) |
| assign(to:) | Assigns the latest value to a property | publisher.assign(to: &object.property) |
| sink() | Handles received values and completion | publisher.sink(receiveValue: { value in ... }) |
Note: Always store subscriptions in a
Set<AnyCancellable> to prevent memory leaks.Warning: Failing to store subscriptions can lead to unexpected behavior or crashes.
9. Using Schedulers
Example: Configuring a Scheduler
import Combine
let scheduler = DispatchQueue.main
let publisher = PassthroughSubject<String, Never>()
publisher
.receive(on: scheduler)
.sink { value in
print("Received on main queue: \\(value)")
}
.store(in: &bag)
| Scheduler | Description | Usage |
|---|---|---|
| RunLoop | Runs tasks on the main run loop | RunLoop.main |
| DispatchQueue | Runs tasks on a specific dispatch queue | DispatchQueue.global() |
| OperationQueue | Runs tasks on an operation queue | OperationQueue() |
Note: Using
receive(on:) ensures that downstream operators and subscribers execute on the specified scheduler.Warning: Avoid using
DispatchQueue.main for heavy computation as it can block the UI thread.Key Points
- Schedulers control where and when
Combinepublishers and subscribers execute. receive(on:)is used to specify the scheduler for downstream operations.- Use
DispatchQueue.global()for background processing to avoid blocking the main thread.
10. Integrating Combine with SwiftUI
Example: Binding to a Published Property
import SwiftUI
import Combine
class DataModel: ObservableObject {
@Published var userName = "John"
}
struct ContentView: View {
@StateObject private var model = DataModel()
var body: some View {
TextField("Enter name", text: $model.userName)
}
}
| Property | Type | Description |
|---|---|---|
| @StateObject | StateObject<T> |
Manages the lifecycle of a state object |
| @Published | Published<T> |
Triggers view updates when value changes |
Note: Use
@StateObject for objects that manage state, and @Published to expose properties that should trigger view updates.Example: Handling Asynchronous Events
import SwiftUI
import Combine
class NetworkService: ObservableObject {
@Published var data: String = ""
private var cancellables = Set<AnyCancellable>()
func fetchData() {
URLSession.shared.dataTaskPublisher(for: URL(string: "https://api.example.com/data")!)
.map { $0.data }
.decode(type: String.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] value in
self?.data = value
}
)
.store(in: &cancellables)
}
}
struct ContentView: View {
@StateObject private var networkService = NetworkService()
var body: some View {
Text(networkService.data)
.onAppear {
networkService.fetchData()
}
}
}