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:
swiftimport 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:
swiftlet publisher = ["Apple", "Pear", "Banana"].publisher let subscriber = publisher.sink { value in print("Received: \(value)") }
Subjects
PassthroughSubject
swiftclass 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
swiftclass 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, 10
filter
swift(1...10).publisher .filter { $0 % 2 == 0 } .sink { even in print("\(even)") } // Output: 2, 4, 6, 8, 10
debounce
swiftclass 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
swiftstruct 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
swiftclass 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
swiftenum 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
swiftstruct 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!