Swift Combine
Apple's reactive programming framework for handling asynchronous events
🔄 What is Swift Combine?
Combine is Apple's reactive programming framework that provides a declarative Swift API for processing values over time. It handles asynchronous events by combining event-processing operators, making complex data flows easier to manage and understand.
import Combine
// Simple publisher that emits values
let publisher = Just("Hello, Combine!")
// Subscribe to receive values
let cancellable = publisher
.sink { value in
print(value) // "Hello, Combine!"
}
Key Combine Concepts
Publishers
Sources that emit values over time
let publisher = Just(42)
let arrayPublisher = [1, 2, 3].publisher
Subscribers
Receivers that consume published values
publisher.sink { value in
print("Received: \(value)")
}
Operators
Transform and filter published values
publisher
.map { $0 * 2 }
.filter { $0 > 10 }
Subjects
Publishers you can send values to
let subject = PassthroughSubject()
subject.send("Hello")
🔹 Basic Publishers and Subscribers
Create and consume data streams with publishers and subscribers:
import Combine
// Simple publishers
let justPublisher = Just("Single Value")
let arrayPublisher = [1, 2, 3, 4, 5].publisher
let rangePublisher = (1...10).publisher
// Basic subscription with sink
var cancellables = Set()
arrayPublisher
.sink { value in
print("Array value: \(value)")
}
.store(in: &cancellables)
// Subscription with completion handling
rangePublisher
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Range publisher finished")
case .failure(let error):
print("Error: \(error)")
}
},
receiveValue: { value in
print("Range value: \(value)")
}
)
.store(in: &cancellables)
// Timer publisher
let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
timerPublisher
.sink { date in
print("Timer tick: \(date)")
}
.store(in: &cancellables)
🔹 Subjects for Manual Control
Use subjects to manually send values to subscribers:
import Combine
var cancellables = Set()
// PassthroughSubject - doesn't hold current value
let passthroughSubject = PassthroughSubject()
passthroughSubject
.sink { value in
print("Passthrough received: \(value)")
}
.store(in: &cancellables)
// Send values
passthroughSubject.send("First message")
passthroughSubject.send("Second message")
// CurrentValueSubject - holds and replays current value
let currentValueSubject = CurrentValueSubject(0)
currentValueSubject
.sink { value in
print("Current value: \(value)")
}
.store(in: &cancellables)
// Update the current value
currentValueSubject.send(10)
currentValueSubject.send(20)
// Access current value directly
print("Direct access: \(currentValueSubject.value)")
// Complete the subjects
passthroughSubject.send(completion: .finished)
currentValueSubject.send(completion: .finished)
🔹 Operators for Data Transformation
Transform and filter data streams with operators:
import Combine
var cancellables = Set()
// Map - transform values
[1, 2, 3, 4, 5].publisher
.map { $0 * $0 } // Square each number
.sink { print("Squared: \($0)") }
.store(in: &cancellables)
// Filter - only pass certain values
(1...10).publisher
.filter { $0 % 2 == 0 } // Only even numbers
.sink { print("Even: \($0)") }
.store(in: &cancellables)
// CompactMap - transform and remove nils
["1", "2", "abc", "4", "xyz"].publisher
.compactMap { Int($0) } // Convert to Int, remove failures
.sink { print("Valid number: \($0)") }
.store(in: &cancellables)
// Reduce - combine all values into one
[1, 2, 3, 4, 5].publisher
.reduce(0, +) // Sum all values
.sink { print("Sum: \($0)") }
.store(in: &cancellables)
// Scan - running total
[1, 2, 3, 4, 5].publisher
.scan(0, +) // Running sum
.sink { print("Running sum: \($0)") }
.store(in: &cancellables)
// RemoveDuplicates - filter out consecutive duplicates
[1, 1, 2, 2, 2, 3, 3, 1].publisher
.removeDuplicates()
.sink { print("Unique: \($0)") }
.store(in: &cancellables)
// Debounce - wait for pause in values
let subject = PassthroughSubject()
subject
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.sink { print("Debounced: \($0)") }
.store(in: &cancellables)
// Simulate rapid input
subject.send("a")
subject.send("ab")
subject.send("abc") // Only this will be printed after 500ms
🔹 Combining Multiple Publishers
Work with multiple data streams simultaneously:
import Combine
var cancellables = Set()
// CombineLatest - combine latest values from multiple publishers
let publisher1 = PassthroughSubject()
let publisher2 = PassthroughSubject()
Publishers.CombineLatest(publisher1, publisher2)
.sink { text, number in
print("Combined: \(text) - \(number)")
}
.store(in: &cancellables)
publisher1.send("Hello")
publisher2.send(1) // Prints: "Combined: Hello - 1"
publisher1.send("World")
publisher2.send(2) // Prints: "Combined: World - 2"
// Zip - pair values in order
let names = ["Alice", "Bob", "Charlie"].publisher
let ages = [25, 30, 35].publisher
Publishers.Zip(names, ages)
.sink { name, age in
print("\(name) is \(age) years old")
}
.store(in: &cancellables)
// Merge - combine values from multiple publishers of same type
let publisher3 = PassthroughSubject()
let publisher4 = PassthroughSubject()
Publishers.Merge(publisher3, publisher4)
.sink { value in
print("Merged: \(value)")
}
.store(in: &cancellables)
publisher3.send("From publisher 3")
publisher4.send("From publisher 4")
// FlatMap - transform each value into a new publisher
["apple", "banana", "cherry"].publisher
.flatMap { fruit in
Just(fruit.uppercased())
}
.sink { print("Uppercase: \($0)") }
.store(in: &cancellables)
🔹 Error Handling
Handle errors gracefully in your data streams:
import Combine
enum NetworkError: Error {
case invalidURL
case noData
case decodingError
}
var cancellables = Set()
// Publisher that might fail
let faultyPublisher = PassthroughSubject()
// Catch errors and provide fallback
faultyPublisher
.catch { error in
Just("Error occurred: \(error)")
}
.sink { value in
print("Received: \(value)")
}
.store(in: &cancellables)
// Send error
faultyPublisher.send(completion: .failure(.noData))
// Retry on failure
let retryPublisher = PassthroughSubject()
var attemptCount = 0
retryPublisher
.map { value -> Int in
attemptCount += 1
if attemptCount < 3 {
throw NetworkError.noData
}
return value
}
.retry(2) // Retry up to 2 times
.catch { _ in Just(-1) } // Fallback value
.sink { value in
print("Final value: \(value)")
}
.store(in: &cancellables)
retryPublisher.send(42)
// ReplaceError - replace any error with a value
let errorPronePublisher = Fail(error: .invalidURL)
errorPronePublisher
.replaceError(with: "Default value")
.sink { value in
print("Safe value: \(value)")
}
.store(in: &cancellables)
🔹 Combine with SwiftUI
Integrate Combine with SwiftUI for reactive UIs:
import SwiftUI
import Combine
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [String] = []
@Published var isLoading = false
private var cancellables = Set()
init() {
// Reactive search with debouncing
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.sink { [weak self] searchText in
self?.performSearch(searchText)
}
.store(in: &cancellables)
}
private func performSearch(_ query: String) {
guard !query.isEmpty else {
results = []
return
}
isLoading = true
// Simulate API call
Just(query)
.delay(for: .seconds(1), scheduler: RunLoop.main)
.map { query in
// Mock search results
["Result 1 for \(query)", "Result 2 for \(query)", "Result 3 for \(query)"]
}
.sink { [weak self] searchResults in
self?.results = searchResults
self?.isLoading = false
}
.store(in: &cancellables)
}
}
struct SearchView: View {
@StateObject private var viewModel = SearchViewModel()
var body: some View {
NavigationView {
VStack {
TextField("Search...", text: $viewModel.searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
if viewModel.isLoading {
ProgressView("Searching...")
.padding()
} else {
List(viewModel.results, id: \.self) { result in
Text(result)
}
}
Spacer()
}
.navigationTitle("Search")
}
}
}
🔹 Combine Best Practices
Guidelines for effective Combine usage:
✅ Do:
- Store cancellables to prevent memory leaks
- Use weak self in closures to avoid retain cycles
- Debounce user input to avoid excessive API calls
- Handle errors gracefully with catch and replaceError
- Use @Published for reactive properties in ObservableObject
❌ Don't:
- Forget to store cancellables - subscriptions will be cancelled immediately
- Create retain cycles with strong self references
- Overuse Combine for simple, one-time operations
- Ignore error handling in publishers that can fail