# Combine Framework: Reaktive Programmierung in Swift > Combine meistern, um asynchrone Datenströme in Swift zu beherrschen: Publishers, Subscribers, Operators und fortgeschrittene Muster für iOS-Apps. - Published: 2026-02-18 - Updated: 2026-04-28 - Author: SharpSkill - Tags: combine, swift, ios, reactive, async - Reading time: 14 min --- Reaktive Programmierung verändert grundlegend, wie asynchrone Ereignisse und Datenströme in iOS-Anwendungen behandelt werden. Combine, Apples natives Framework, bietet einen deklarativen und typsicheren Ansatz, um komplexe Daten-Pipelines zu orchestrieren. Dieser Leitfaden führt von den Grundkonzepten bis hin zu produktionsreifen Mustern. > **Warum Combine statt RxSwift?** > > Combine ist ab iOS 13 fest integriert, liefert dank Apples Optimierungen bessere Leistung und fügt sich nahtlos in SwiftUI ein. Keine externen Abhängigkeiten zu pflegen. ## Combine Kernkonzepte Combine baut auf drei zentralen Konzepten auf: **Publishers**, die Werte ausgeben, **Subscribers**, die diese empfangen, und **Operators**, die Daten dazwischen transformieren. Diese Architektur erlaubt es, reaktive und kombinierbare Daten-Pipelines aufzubauen. ### Publisher: die Datenquelle Ein Publisher ist ein Typ, der eine Folge von Werten über die Zeit emittieren kann. Jeder Publisher deklariert zwei zugehörige Typen: den Typ des emittierten Werts (`Output`) und den Typ möglicher Fehler (`Failure`). So lassen sich verschiedene Publisher-Typen erstellen: ```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")) } } ``` Der Typ `Never` für Fehler bedeutet, dass der Publisher niemals scheitern kann. Diese Garantie zur Compile-Zeit vereinfacht den Code zur Fehlerbehandlung. ### Subscriber: Werte empfangen Ein Subscriber abonniert einen Publisher, um dessen Werte zu erhalten. Die Methode `sink` ist die häufigste Art, einen Subscriber zu erstellen. Sie erwartet zwei Closures: eine für Fehler oder Abschluss und eine für jeden empfangenen Wert: ```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 ``` > **Vorsicht vor Memory Leaks** > > Das von `sink()` zurückgegebene `AnyCancellable` muss stets gespeichert werden. Ohne Referenz wird das Abonnement automatisch beendet und kein Wert empfangen. ## Daten transformieren mit Operators Operators sind das Herzstück von Combine. Sie ermöglichen es, Datenströme deklarativ zu transformieren, zu filtern und zu kombinieren. Jeder Operator gibt einen neuen Publisher zurück, sodass sie sich verketten lassen. ### Wesentliche Transformations-Operators Transformations-Operators verändern jeden emittierten Wert. `map` transformiert Werte, `flatMap` flacht verschachtelte Publisher ab und `compactMap` filtert nil-Werte heraus: ```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 steuern, welche Werte die Pipeline durchlaufen. Sie sind unverzichtbar, um unnötige Verarbeitung zu vermeiden und die Performance zu optimieren: ```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 ``` ## Mehrere Publisher kombinieren Reale Anwendungen müssen häufig mehrere Datenquellen verbinden. Combine bietet verschiedene Operators, um diese Mehrfachströme zu orchestrieren. ### CombineLatest und Zip `combineLatest` emittiert, sobald irgendein Publisher emittiert, und kombiniert dabei mit den letzten Werten der anderen. `zip` wartet, bis alle Publisher emittiert haben, bevor kombiniert wird: ```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 zur Vereinheitlichung von Strömen `merge` führt mehrere Publisher desselben Typs zu einem einzigen Strom zusammen. Werte treffen in der Reihenfolge ihrer Emission ein, unabhängig vom Absender: ```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! ``` ## Fehlerbehandlung in Combine Fehlerbehandlung ist fest in den Kern von Combine integriert. Der `Failure`-Typ der Publisher erlaubt es dem Compiler zu prüfen, dass alle Fehler behandelt werden. ### Recovery-Strategien Combine bietet mehrere Operators für die Fehlerbehandlung: `catch`, um durch einen anderen Publisher zu ersetzen, `retry`, um es erneut zu versuchen, und `replaceError` für einen Standardwert: ```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 versus Error** > > Mit `setFailureType(to:)` lässt sich ein `Never`-Publisher in einen umwandeln, der scheitern kann; `replaceError(with:)` oder `catch` machen den umgekehrten Weg. ## MVVM-Muster mit Combine Combine fügt sich natürlich in das MVVM-Muster (Model-View-ViewModel) ein. Das ViewModel stellt Publisher bereit, die die View beobachtet, und schafft so eine reaktive Bindung zwischen Daten und Oberfläche. ### Vollständiges reaktives ViewModel Hier ein Beispiel-ViewModel für eine Benutzerliste mit Suche, Ladezustand und Fehlerbehandlung: ```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 mit Combine und URLSession URLSession integriert Combine nativ über `dataTaskPublisher`. So lässt sich ein wiederverwendbarer Netzwerkdienst aufbauen: ```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-Integration Combine und SwiftUI bilden ein starkes Duo. Die `@Published`-Properties eines `ObservableObject` lösen automatisch View-Updates aus. ### SwiftUI-View mit Combine-ViewModel So wird das ViewModel an eine SwiftUI-View angebunden: ```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) } } ``` ## Fortgeschrittene Muster ### Cancellation und automatische Bereinigung Das Lifecycle-Management von Subscriptions ist entscheidend, um Memory Leaks zu vermeiden. Das `cancellables`-Muster mit `AnyCancellable` sorgt für automatische Bereinigung: ```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 für Threading Schedulers steuern, in welchem Thread Operationen ausgeführt werden. `subscribe(on:)` für Hintergrundarbeit und `receive(on:)` für 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() } ``` ## Fazit Combine bietet einen mächtigen, deklarativen Ansatz, um asynchrone Datenströme in iOS-Apps zu beherrschen. Die wichtigsten Punkte: ✅ **Publishers** emittieren Werte über die Zeit ✅ **Subscribers** empfangen und verarbeiten diese Werte ✅ **Operators** transformieren und kombinieren Ströme ✅ `AnyCancellable` verwaltet den Subscription-Lifecycle ✅ `@Published` mit SwiftUI erzeugt automatische reaktive Bindings ✅ **Schedulers** steuern das Threading für optimale Performance Wer Combine beherrscht, baut robuste, wartbare und reaktive iOS-Apps. Die native Integration mit SwiftUI macht es zu einem unverzichtbaren Werkzeug der modernen iOS-Entwicklung. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/ios/combine-reactive-programming-swift