Combine is Apple's framework introduced in 2019 for processing values over time declaratively. It brings reactive programming paradigm to Swift.
Core Concepts
Publisher
Object that emits values over time:
swift
import Combine
class NewsProvider {
func fetchNews() -> AnyPublisher<String, Never> {
return Future { promise in
// Asynchronous operation
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.success("Breaking news!"))
}
}
.eraseToAnyPublisher()
}
}Subscriber
Object that receives values from a publisher:
swift
let publisher = ["Apple", "Pear", "Banana"].publisher
let subscriber = publisher.sink { value in
print("Received: \(value)")
}Subjects
PassthroughSubject
swift
class MessageService {
let messagePublisher = PassthroughSubject<String, Never>()
func sendMessage(_ message: String) {
messagePublisher.send(message)
}
}
let service = MessageService()
// Subscribe
service.messagePublisher
.sink { message in
print("Message: \(message)")
}
// Send
service.sendMessage("Hello!")
service.sendMessage("Combine is awesome!")CurrentValueSubject
swift
class UserSettings {
let isDarkModeEnabled = CurrentValueSubject<Bool, Never>(false)
func toggleTheme() {
isDarkModeEnabled.send(!isDarkModeEnabled.value)
}
}
let settings = UserSettings()
// Receive current value and future values
settings.isDarkModeEnabled
.sink { isEnabled in
print("Dark mode: \(isEnabled)")
}
// Read current value
print("Current: \(settings.isDarkModeEnabled.value)")Operators
map
swift
[1, 2, 3, 4, 5].publisher
.map { $0 * 2 }
.sink { doubled in
print("\(doubled)")
}
// Output: 2, 4, 6, 8, 10filter
swift
(1...10).publisher
.filter { $0 % 2 == 0 }
.sink { even in
print("\(even)")
}
// Output: 2, 4, 6, 8, 10debounce
swift
class SearchViewModel: ObservableObject {
@Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.removeDuplicates()
.sink { text in
self.performSearch(text)
}
.store(in: &cancellables)
}
func performSearch(_ text: String) {
print("Searching: \(text)")
}
}flatMap
swift
struct User {
let id: Int
let name: String
}
struct Post {
let id: Int
let userId: Int
let title: String
}
func fetchUser(id: Int) -> AnyPublisher<User, Error> {
// User fetch operation
return Just(User(id: id, name: "John"))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
func fetchPosts(userId: Int) -> AnyPublisher<[Post], Error> {
// Posts fetch operation
return Just([
Post(id: 1, userId: userId, title: "First post")
])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// Usage
fetchUser(id: 1)
.flatMap { user in
fetchPosts(userId: user.id)
.map { posts in (user, posts) }
}
.sink { completion in
print("Completed: \(completion)")
} receiveValue: { (user, posts) in
print("\(user.name) has \(posts.count) posts")
}@Published Property Wrapper
swift
class ViewModel: ObservableObject {
@Published var items: [String] = []
@Published var isLoading: Bool = false
private var cancellables = Set<AnyCancellable>()
init() {
// Monitor changes
$items
.sink { items in
print("Items changed: \(items.count)")
}
.store(in: &cancellables)
}
func loadItems() {
isLoading = true
// Async load
Just(["Apple", "Pear", "Banana"])
.delay(for: .seconds(2), scheduler: RunLoop.main)
.sink { [weak self] items in
self?.items = items
self?.isLoading = false
}
.store(in: &cancellables)
}
}Error Handling
swift
enum NetworkError: Error {
case invalidURL
case requestFailed
}
func fetchData() -> AnyPublisher<Data, NetworkError> {
return Future<Data, NetworkError> { promise in
// Simulated network request
let success = Bool.random()
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
if success {
promise(.success(Data()))
} else {
promise(.failure(.requestFailed))
}
}
}
.eraseToAnyPublisher()
}
// Error handling
fetchData()
.catch { error in
print("Error: \(error)")
return Just(Data())
}
.sink { data in
print("Data received: \(data.count) bytes")
}SwiftUI Integration
swift
struct SearchView: View {
@StateObject private var viewModel = SearchViewModel()
var body: some View {
List(viewModel.results, id: \.self) { result in
Text(result)
}
.searchable(text: $viewModel.searchText)
.onAppear {
viewModel.load()
}
}
}
class SearchViewModel: ObservableObject {
@Published var searchText = ""
@Published var results: [String] = []
private var cancellables = Set<AnyCancellable>()
func load() {
$searchText
.debounce(for: .milliseconds(500), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap { query in
self.search(query)
}
.sink { completion in
} receiveValue: { results in
self.results = results
}
.store(in: &cancellables)
}
private func search(_ query: String) -> AnyPublisher<[String], Never> {
// Search implementation
return Just([])
.eraseToAnyPublisher()
}
}Combine Best Practices
- 1Memory management: Properly manage cancellables
- 2Error handling: Keep error types consistent
- 3Threading: Use
receive(on:)for thread management - 4Debounce: Use debounce for user input
- 5Cleanup: Cancel subscriptions when view disappears
Combine integrates perfectly with SwiftUI for asynchronous programming!