Publishers & Subscribers
Understanding Combine Architecture
Combine is Apple's reactive programming framework. It uses three core types: Publishers emit values, Subscribers receive values, and Operators transform values between them.
Creating Publishers
Use Just, Future, or URLSession publishers to emit values.
import Combine
// Just emits a single value and completes
let publisher = Just("Hello Combine")
let _ = publisher.sink { value in
print(value) // "Hello Combine"
}
// Future emits a value asynchronously
let future = Future<String, Error> { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
promise(.success("Async value"))
}
}
let _ = future.sink(
receiveCompletion: { _ in },
receiveValue: { print($0) }
)
// URLSession publisher for network requests
let url = URL(string: "https://api.example.com/data")!
let dataPublisher = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: MyModel.self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
Subscribing to Publishers
Subscribers receive values and completion events from publishers.
let cancellable = publisher.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
case .failure(let error):
print("Error: \(error)")
}
},
receiveValue: { value in
print("Received: \(value)")
}
)
// Store cancellables to prevent deallocation
var cancellables = Set<AnyCancellable>()
publisher.sink { print($0) }.store(in: &cancellables)
Managing Subscription Lifecycle
Always store cancellables to keep subscriptions alive.
class ViewModel: ObservableObject {
@Published var data: [String] = []
private var cancellables = Set<AnyCancellable>()
func fetchData() {
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [String].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { _ in },
receiveValue: { [weak self] items in
self?.data = items
}
)
.store(in: &cancellables)
}
}
Operators
Transforming Values
Operators modify, filter, and combine values emitted by publishers.
let numbers = [1, 2, 3, 4, 5].publisher
// Map transforms each value
numbers
.map { $0 * 2 }
.sink { print($0) } // 2, 4, 6, 8, 10
// Filter keeps values matching a condition
numbers
.filter { $0 % 2 == 0 }
.sink { print($0) } // 2, 4
// Reduce combines all values into one
numbers
.reduce(0, +)
.sink { print($0) } // 15
// Remove duplicates
[1, 1, 2, 2, 3].publisher
.removeDuplicates()
.sink { print($0) } // 1, 2, 3
Combining Publishers
Merge, combine, and zip multiple publishers together.
let publisher1 = [1, 2, 3].publisher
let publisher2 = ["A", "B", "C"].publisher
// CombineLatest emits when either publisher emits
publisher1.combineLatest(publisher2)
.sink { num, letter in
print("\(num)\(letter)") // 1A, 2A, 3A, 3B, 3C
}
// Zip pairs values from both publishers
publisher1.zip(publisher2)
.sink { num, letter in
print("\(num)\(letter)") // 1A, 2B, 3C
}
// Merge combines all values as they arrive
publisher1.merge(with: publisher2)
.sink { print($0) }
Timing and Scheduling
Control when values are emitted and received.
let publisher = [1, 2, 3].publisher
// Delay emissions
publisher
.delay(for: .seconds(2), scheduler: DispatchQueue.main)
.sink { print($0) }
// Throttle rapid emissions
publisher
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.sink { print($0) }
// Receive on specific scheduler
publisher
.receive(on: DispatchQueue.main)
.sink { print($0) }
Error Handling
Handle errors gracefully in Combine chains.
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.mapError { $0 as Error }
.retry(3) // Retry 3 times on failure
.catch { error in
// Return fallback value
return Just(Data())
}
.sink { data in
print("Received data")
}
Subjects & PassthroughSubject
What Are Subjects?
Subjects are publishers that allow you to manually send values. They bridge imperative code to reactive streams.
PassthroughSubject
Emits values to subscribers but does not store them. New subscribers miss previous values.
import Combine
let subject = PassthroughSubject<String, Never>()
let cancellable = subject.sink { value in
print("Received: \(value)")
}
subject.send("Hello")
subject.send("World")
// Both values are received
// New subscriber misses previous values
let cancellable2 = subject.sink { value in
print("New subscriber received: \(value)")
}
subject.send("After new subscriber")
// Only "After new subscriber" is received by cancellable2
CurrentValueSubject
Stores the current value and emits it to new subscribers immediately.
let currentValue = CurrentValueSubject<Int, Never>(0)
let cancellable = currentValue.sink { value in
print("Value: \(value)")
} // Prints 0 immediately
currentValue.send(1)
let cancellable2 = currentValue.sink { value in
print("New subscriber: \(value)")
} // Prints 1 immediately
Creating Custom Publishers
Build your own publishers for complex data sources.
struct TimerPublisher: Publisher {
typealias Output = Date
typealias Failure = Never
let interval: TimeInterval
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
let subscription = TimerSubscription(
subscriber: subscriber,
interval: interval
)
subscriber.receive(subscription: subscription)
}
}
class TimerSubscription<S: Subscriber>: Subscription where S.Input == Date {
private var subscriber: S?
private var timer: Timer?
init(subscriber: S, interval: TimeInterval) {
self.subscriber = subscriber
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
_ = self?.subscriber?.receive(Date())
}
}
func request(_ demand: Subscribers.Demand) { }
func cancel() {
timer?.invalidate()
subscriber = nil
}
}
Using Subjects in ViewModels
Subjects are ideal for connecting UI events to business logic.
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [Result] = []
private var cancellables = Set<AnyCancellable>()
private let searchSubject = PassthroughSubject<String, Never>()
init() {
searchSubject
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
.removeDuplicates()
.flatMap { query in
APIClient.search(query: query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.assign(to: &$results)
}
func search(_ query: String) {
searchSubject.send(query)
}
}
Quiz
1. What is the difference between PassthroughSubject and CurrentValueSubject?
2. What does the sink operator do?
3. How do you prevent a Combine subscription from being deallocated?
4. What does the debounce operator do?
Flashcards
Question
What is a Publisher in Combine?
Click to reveal answer
Answer
A type that emits values over time to subscribers, forming the data source of a Combine pipeline.
Question
What is AnyCancellable used for?
Click to reveal answer
Answer
It represents a subscription that can be cancelled. Store in a Set to manage subscription lifecycle.
Question
What does .assign(to:) do?
Click to reveal answer
Answer
Automatically assigns received values to a property using key path, commonly used with @Published properties.
Question
When would you use PassthroughSubject?
Click to reveal answer
Answer
When you need to manually send values to subscribers, such as bridging UI events to reactive streams.
Revision Notes
Key Takeaways
- 1. Publishers emit values, subscribers receive them
- 2. Operators transform, filter, and combine streams
- 3. Store AnyCancellable to manage subscription lifecycle
- 4. PassthroughSubject for manual emission, CurrentValueSubject stores values
- 5. Combine integrates naturally with @Published in SwiftUI
Interview Tips
- • Explain the Publisher-Subscriber-Operator architecture
- • Discuss when to use Combine vs async/await
- • Describe how debounce and throttle differ
Cheat Sheet
Combine Quick Reference
- Just(value) - Emit single value
- Future - Async value emission
- .sink() - Subscribe and receive values
- .assign(to:) - Assign to property
- .map() / .filter() / .reduce() - Transform values
- .combineLatest() / .zip() / .merge() - Combine publishers
- .debounce() / .throttle() - Timing operators
- PassthroughSubject - Manual value sending
- CurrentValueSubject - Stores current value
- AnyCancellable - Subscription management