# Combine Framework: Reactief Programmeren in Swift > Beheers Combine voor het verwerken van asynchrone datastromen in Swift: Publishers, Subscribers, Operators en geavanceerde patronen voor iOS-apps. - Published: 2026-02-18 - Updated: 2026-04-28 - Author: SharpSkill - Tags: combine, swift, ios, reactive, async - Reading time: 14 min --- Reactief programmeren verandert grondig hoe asynchrone events en datastromen in iOS-applicaties worden verwerkt. Combine, het native framework van Apple, biedt een declaratieve en type-safe aanpak om complexe datapijplijnen te orkestreren. Deze gids loopt van de basisconcepten tot productieklare patronen. > **Waarom Combine in plaats van RxSwift?** > > Combine is geïntegreerd in iOS 13+, levert betere prestaties dankzij Apples optimalisaties en sluit naadloos aan op SwiftUI. Geen externe afhankelijkheden om te beheren. ## Kernconcepten van Combine Combine is opgebouwd uit drie kernconcepten: **Publishers** die waarden uitzenden, **Subscribers** die ze ontvangen en **Operators** die de data ertussenin transformeren. Deze architectuur maakt het mogelijk om reactieve, samen te stellen datapijplijnen te bouwen. ### Publisher: de databron Een Publisher is een type dat een reeks waarden in de tijd kan uitzenden. Elke Publisher declareert twee bijbehorende types: het type van de uitgezonden waarde (`Output`) en het type van de mogelijke fout (`Failure`). Zo maak je verschillende soorten Publishers: ```swift // PublisherBasics.swift import Combine // Just: emits a single value then completes // Useful for converting a simple value to a Publisher let singleValue = Just("Hello Combine") // CurrentValueSubject: stores and emits the current value // Perfect for representing state that changes over time let counter = CurrentValueSubject(0) // PassthroughSubject: emits values without storing them // Ideal for one-time events (taps, notifications) let buttonTaps = PassthroughSubject() // Future: emits a single value asynchronously // Wraps an async operation that returns a result let asyncOperation = Future { promise in // Simulate a network call DispatchQueue.global().asyncAfter(deadline: .now() + 1) { promise(.success("Data loaded")) } } ``` Het type `Never` voor fouten betekent dat de Publisher nooit kan falen. Dat is een garantie tijdens compileren die de foutafhandelingscode vereenvoudigt. ### Subscriber: waarden ontvangen Een Subscriber abonneert zich op een Publisher om diens waarden te ontvangen. De methode `sink` is de meest gebruikelijke manier om een Subscriber te maken. Hij neemt twee closures: één voor fouten of voltooiing en één voor elke ontvangen waarde: ```swift // SubscriberBasics.swift import Combine // Variable to store subscriptions // Without this reference, the subscription would be immediately cancelled var cancellables = Set() let publisher = ["Swift", "Combine", "iOS"].publisher // sink() creates a Subscriber that receives values publisher .sink( // Called when the Publisher completes or fails receiveCompletion: { completion in switch completion { case .finished: print("✅ Completed successfully") case .failure(let error): print("❌ Error: \(error)") } }, // Called for each emitted value receiveValue: { value in print("Received: \(value)") } ) // store() keeps a reference to the subscription .store(in: &cancellables) // Output: // Received: Swift // Received: Combine // Received: iOS // ✅ Completed successfully ``` > **Pas op voor memory leaks** > > Bewaar altijd de `AnyCancellable` die `sink()` teruggeeft. Zonder referentie wordt het abonnement automatisch geannuleerd en wordt geen enkele waarde ontvangen. ## Data transformeren met Operators Operators vormen het hart van Combine. Ze maken het mogelijk om datastromen declaratief te transformeren, te filteren en te combineren. Elke Operator levert een nieuwe Publisher op, waardoor ze gekoppeld kunnen worden. ### Essentiële transformatie-Operators Transformatie-Operators wijzigen elke uitgezonden waarde. `map` transformeert waarden, `flatMap` plat geneste Publishers af en `compactMap` filtert nil-waarden eruit: ```swift // TransformOperators.swift import Combine var cancellables = Set() // map: transforms each value // Equivalent to map on arrays [1, 2, 3, 4, 5].publisher .map { $0 * 2 } // Multiply each number by 2 .sink { print("Doubled: \($0)") } .store(in: &cancellables) // Output: 2, 4, 6, 8, 10 // compactMap: transforms AND filters out nil // Useful for optional conversions ["1", "two", "3", "four", "5"].publisher .compactMap { Int($0) } // Convert to Int, ignore failures .sink { print("Valid number: \($0)") } .store(in: &cancellables) // Output: 1, 3, 5 // flatMap: flattens nested Publishers // Essential for chaining async operations struct User { let id: Int; let name: String } func fetchUser(id: Int) -> AnyPublisher { // Simulate an API call Just(User(id: id, name: "User \(id)")) .delay(for: .milliseconds(100), scheduler: RunLoop.main) .eraseToAnyPublisher() } [1, 2, 3].publisher .flatMap { id in fetchUser(id: id) } // Each ID becomes an API call .sink { user in print("User: \(user.name)") } .store(in: &cancellables) ``` ### Filter-Operators Filter-Operators bepalen welke waarden de pijplijn doorlopen. Ze zijn essentieel om onnodige verwerking te vermijden en de prestaties te optimaliseren: ```swift // FilterOperators.swift import Combine var cancellables = Set() let numbers = [1, 2, 2, 3, 3, 3, 4, 5, 5].publisher // filter: keeps only values that satisfy the condition numbers .filter { $0 > 2 } // Keep only numbers > 2 .sink { print("Filtered: \($0)") } .store(in: &cancellables) // Output: 3, 3, 3, 4, 5, 5 // removeDuplicates: removes consecutive identical values numbers .removeDuplicates() // Eliminate consecutive duplicates .sink { print("Without duplicates: \($0)") } .store(in: &cancellables) // Output: 1, 2, 3, 4, 5 // debounce: waits for a pause before emitting // Perfect for real-time search let searchText = PassthroughSubject() searchText .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() // Ignore if text hasn't changed .sink { query in print("Search: \(query)") // Launch API call here } .store(in: &cancellables) // Simulate rapid typing searchText.send("S") searchText.send("Sw") searchText.send("Swi") searchText.send("Swift") // Only "Swift" is emitted after 300ms ``` ## Meerdere Publishers combineren Reële applicaties moeten vaak verschillende databronnen samenbrengen. Combine biedt diverse Operators om deze meerdere stromen te orkestreren. ### CombineLatest en Zip `combineLatest` zendt uit zodra eender welke Publisher een waarde uitzendt en combineert met de laatste waarden van de anderen. `zip` wacht tot alle Publishers hebben uitgezonden voordat het combineert: ```swift // CombiningPublishers.swift import Combine var cancellables = Set() // Simulate a form with validation let email = CurrentValueSubject("") let password = CurrentValueSubject("") // combineLatest: combines the latest values from each Publisher // Emits on every change from either source Publishers.CombineLatest(email, password) .map { email, password in // Validate that email contains @ and password > 6 chars let isEmailValid = email.contains("@") let isPasswordValid = password.count >= 6 return isEmailValid && isPasswordValid } .sink { isFormValid in print("Form valid: \(isFormValid)") } .store(in: &cancellables) email.send("user@example.com") // false (password empty) password.send("123456") // true (both are valid) // zip: waits for one value from each Publisher before emitting // Useful for synchronizing parallel operations let firstAPI = PassthroughSubject() let secondAPI = PassthroughSubject() Publishers.Zip(firstAPI, secondAPI) .sink { stringValue, intValue in print("Received pair: \(stringValue), \(intValue)") } .store(in: &cancellables) firstAPI.send("Hello") // No emission, waiting for secondAPI secondAPI.send(42) // Emits: ("Hello", 42) firstAPI.send("World") // No emission, waiting for secondAPI secondAPI.send(100) // Emits: ("World", 100) ``` ### Merge om stromen te bundelen `merge` voegt meerdere Publishers van hetzelfde type samen tot één enkele stroom. De waarden komen aan in volgorde van uitzending, ongeacht welke Publisher ze stuurde: ```swift // MergePublishers.swift import Combine var cancellables = Set() // Multiple user notification sources let pushNotifications = PassthroughSubject() let localNotifications = PassthroughSubject() let inAppMessages = PassthroughSubject() // Merge unifies all streams into one Publishers.Merge3(pushNotifications, localNotifications, inAppMessages) .sink { message in // Handle all notifications the same way print("📬 Notification: \(message)") } .store(in: &cancellables) pushNotifications.send("New message") // 📬 Notification: New message localNotifications.send("Reminder: meeting") // 📬 Notification: Reminder: meeting inAppMessages.send("Welcome!") // 📬 Notification: Welcome! ``` ## Foutafhandeling in Combine Foutafhandeling zit ingebakken in de kern van Combine. Het type `Failure` van Publishers laat de compiler controleren of alle fouten worden afgehandeld. ### Herstelstrategieën Combine biedt diverse Operators om fouten af te handelen: `catch` om te vervangen door een andere Publisher, `retry` om opnieuw te proberen en `replaceError` voor een standaardwaarde: ```swift // ErrorHandling.swift import Combine var cancellables = Set() enum APIError: Error { case networkError case invalidResponse case serverError(Int) } // Simulate an API call that can fail func fetchData() -> AnyPublisher { Fail(error: APIError.networkError) .eraseToAnyPublisher() } // retry: retries N times before propagating the error fetchData() .retry(3) // Try up to 3 times .catch { error -> Just in // catch: replaces the error with a fallback Publisher print("Error after 3 attempts: \(error)") return Just("Cached data") // Fallback value } .sink( receiveCompletion: { _ in }, receiveValue: { print("Result: \($0)") } ) .store(in: &cancellables) // replaceError: replaces any error with a fixed value // Simpler than catch when only a default value is needed fetchData() .replaceError(with: "Error - default value") .sink { print("With fallback: \($0)") } .store(in: &cancellables) ``` > **Never tegenover Error** > > Gebruik `setFailureType(to:)` om een `Never`-Publisher om te zetten naar één die kan falen, en `replaceError(with:)` of `catch` voor de omgekeerde richting. ## MVVM-patroon met Combine Combine integreert van nature met het MVVM-patroon (Model-View-ViewModel). Het ViewModel stelt Publishers ter beschikking die de View observeert, waardoor een reactieve binding ontstaat tussen data en interface. ### Volledig reactief ViewModel Hier een voorbeeld van een ViewModel voor een gebruikerslijst met zoekopdracht, laadtoestand en foutafhandeling: ```swift // UserListViewModel.swift import Combine import Foundation // Data model struct User: Codable, Identifiable { let id: Int let name: String let email: String } // ViewModel with reactive state final class UserListViewModel: ObservableObject { // MARK: - Published Properties (observed by SwiftUI) @Published var users: [User] = [] // User list @Published var searchQuery: String = "" // Search text @Published var isLoading: Bool = false // Loading state @Published var errorMessage: String? // Optional error message // MARK: - Private Properties private var cancellables = Set() private let userService: UserServiceProtocol // MARK: - Computed Properties // Filters users based on search query var filteredUsers: [User] { guard !searchQuery.isEmpty else { return users } return users.filter { $0.name.localizedCaseInsensitiveContains(searchQuery) } } // MARK: - Initialization init(userService: UserServiceProtocol = UserService()) { self.userService = userService setupBindings() } // MARK: - Private Methods private func setupBindings() { // Observe searchQuery changes // debounce prevents too frequent calls $searchQuery .debounce(for: .milliseconds(300), scheduler: RunLoop.main) .removeDuplicates() .sink { [weak self] query in // Server-side search logic if needed print("Search updated: \(query)") } .store(in: &cancellables) } // MARK: - Public Methods func loadUsers() { isLoading = true errorMessage = nil userService.fetchUsers() .receive(on: DispatchQueue.main) // Ensure UI updates on main thread .sink( receiveCompletion: { [weak self] completion in self?.isLoading = false if case .failure(let error) = completion { self?.errorMessage = error.localizedDescription } }, receiveValue: { [weak self] users in self?.users = users } ) .store(in: &cancellables) } } ``` ### Service met Combine en URLSession URLSession integreert Combine native via `dataTaskPublisher`. Zo bouw je een herbruikbare netwerkservice: ```swift // UserService.swift import Combine import Foundation protocol UserServiceProtocol { func fetchUsers() -> AnyPublisher<[User], Error> } final class UserService: UserServiceProtocol { private let baseURL = URL(string: "https://api.example.com")! private let session: URLSession private let decoder: JSONDecoder init(session: URLSession = .shared) { self.session = session self.decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase } func fetchUsers() -> AnyPublisher<[User], Error> { let url = baseURL.appendingPathComponent("users") return session.dataTaskPublisher(for: url) // Check HTTP status code .tryMap { data, response in guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } guard 200..<300 ~= httpResponse.statusCode else { throw URLError(.badServerResponse) } return data } // Decode JSON to Swift model .decode(type: [User].self, decoder: decoder) // Erase concrete type to return AnyPublisher .eraseToAnyPublisher() } } ``` ## SwiftUI-integratie Combine en SwiftUI vormen een krachtig duo. De `@Published`-properties van een `ObservableObject` triggeren automatisch view-updates. ### SwiftUI-view met Combine-ViewModel Zo koppel je het ViewModel aan een SwiftUI-view: ```swift // UserListView.swift import SwiftUI struct UserListView: View { // StateObject: creates and owns the ViewModel @StateObject private var viewModel = UserListViewModel() var body: some View { NavigationStack { Group { if viewModel.isLoading { // Centered loading indicator ProgressView("Loading...") } else if let error = viewModel.errorMessage { // Error view with retry button VStack(spacing: 16) { Text("Error: \(error)") .foregroundStyle(.red) Button("Retry") { viewModel.loadUsers() } } } else { // User list List(viewModel.filteredUsers) { user in UserRowView(user: user) } } } .navigationTitle("Users") .searchable(text: $viewModel.searchQuery) // Direct binding .onAppear { viewModel.loadUsers() // Load on first appearance } } } } struct UserRowView: View { let user: User var body: some View { VStack(alignment: .leading, spacing: 4) { Text(user.name) .font(.headline) Text(user.email) .font(.subheadline) .foregroundStyle(.secondary) } .padding(.vertical, 4) } } ``` ## Geavanceerde patronen ### Annulering en automatische opruiming De levenscyclus van abonnementen beheren is cruciaal om memory leaks te voorkomen. Het `cancellables`-patroon met `AnyCancellable` zorgt voor automatische opruiming: ```swift // CancellationPatterns.swift import Combine final class DataManager { // Set of cancellables: automatically cancelled on destruction private var cancellables = Set() // Individual cancellable for fine-grained control private var currentRequest: AnyCancellable? func startPolling() { // Timer that emits every 5 seconds Timer.publish(every: 5, on: .main, in: .common) .autoconnect() // Starts automatically .sink { [weak self] _ in self?.fetchLatestData() } .store(in: &cancellables) } func fetchLatestData() { // Cancel the previous request if it exists currentRequest?.cancel() currentRequest = URLSession.shared .dataTaskPublisher(for: URL(string: "https://api.example.com/data")!) .map(\.data) .decode(type: [String].self, decoder: JSONDecoder()) .replaceError(with: []) .receive(on: DispatchQueue.main) .sink { data in print("Data received: \(data)") } } deinit { // All cancellables are automatically cancelled print("DataManager destroyed, subscriptions cancelled") } } ``` ### Schedulers voor threading Schedulers bepalen op welke thread bewerkingen draaien. Gebruik `subscribe(on:)` voor achtergrondwerk en `receive(on:)` voor UI-updates: ```swift // SchedulerPatterns.swift import Combine import Foundation var cancellables = Set() func loadAndProcessData() -> AnyPublisher { URLSession.shared.dataTaskPublisher(for: apiURL) // Perform parsing on a background thread .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .map(\.data) .decode(type: RawData.self, decoder: JSONDecoder()) // Heavy processing on background thread .map { rawData in // This expensive operation runs in the background processData(rawData) } // Return to main thread for UI .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } ``` ## Conclusie Combine biedt een krachtige en declaratieve aanpak voor het verwerken van asynchrone datastromen in iOS-apps. Belangrijkste punten: ✅ **Publishers** zenden waarden uit in de tijd ✅ **Subscribers** ontvangen en verwerken die waarden ✅ **Operators** transformeren en combineren stromen ✅ `AnyCancellable` beheert de levenscyclus van abonnementen ✅ `@Published` met SwiftUI maakt automatische reactieve bindings ✅ **Schedulers** sturen threading aan voor optimale prestaties Wie Combine beheerst, bouwt robuuste, onderhoudbare en reactieve iOS-apps. De native integratie met SwiftUI maakt het tot een onmisbaar gereedschap voor moderne iOS-ontwikkeling. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/ios/combine-reactive-programming-swift