Combine ist Apples natives reaktives Programmierframework, eingeführt in iOS 13, macOS Catalina, tvOS 13 und watchOS 6. Es bietet eine deklarative Swift-API zur Verarbeitung asynchroner Ereignisse durch das Publisher-Subscriber-Muster und ersetzt Delegaten, Closures und NotificationCenter durch eine einheitliche Kette. Laut Apple, 2025 ist Combine die Grundlage für SwiftUI und moderne iOS-Architekturen und arbeitet eng mit async/await und Structured Concurrency zusammen. Das Framework wurde für die Komposition asynchroner Operationen mit Thread-Sicherheitsgarantien entwickelt.
Wesentliche Punkte
Combine ist ein deklaratives reaktives Programmierframework, das in das Apple SDK integriert ist. Es implementiert das Reactive-Streams-Muster: Publisher produziert Werte, Subscriber konsumiert sie und Operatoren transformieren den Stream dazwischen. Combine löst das Problem von Callbacks und Delegaten, indem es ein einheitliches Kompositionsmodell für beliebige asynchrone Ereignisse bereitstellt — von Netzwerkantworten bis hin zu UI-Änderungen.
Vor Combine verwendeten iOS-Entwickler Drittanbieter-Bibliotheken, hauptsächlich RxSwift. Apple entwickelte Combine als native Alternative mit tiefer Integration in das Ökosystem: Das Framework unterstützt Objective-C über @objc-Brücken, arbeitet mit KVO (Key-Value Observing über NSObject.keyValuePublisher) und NotificationCenter und dient auch als Grundlage für SwiftUI. Alle in SwiftUI veröffentlichten UIKit-Komponenten verwenden Combine intern für Ansichtsaktualisierungen.
Combine wurde mit Blick auf Swift Concurrency entwickelt: Ab iOS 15 kann ein Publisher über .values in AsyncSequence umgewandelt und in for-await-in-Schleifen verwendet werden. Die umgekehrte Umwandlung von Async-Funktionen in Publisher erfolgt über Future. Laut Apple WWDC 2024 bleibt Combine trotz der Einführung von async/await für einmalige asynchrone Aufrufe das empfohlene Framework für die Verarbeitung von Streaming-Daten in UIKit-Anwendungen.
Combine basiert auf drei Protokollen: Publisher (emittiert Werte vom Typ Output, kann mit einem Fehler vom Typ Failure fehlschlagen), Subscriber (empfängt Werte, verwaltet Demand — die Anzahl der angeforderten Elemente), Subscription (stellt die Publisher-Subscriber-Verbindung mit Abbruchmöglichkeit dar). Der Datenkanal wird beim Subscribe initialisiert und endet bei Abbruch, Abschluss oder Fehler. Demand ist ein einzigartiges Combine-Konzept: Der Subscriber teilt dem Publisher mit, wie viele Elemente er verarbeiten kann, und implementiert so Gegensteuerung auf Protokollebene.
Publisher ist ein Protokoll mit zwei zugehörigen Typen: Output (der Typ der emittierten Werte) und Failure (der Fehlertyp, der Error implementiert). Wenn der Stream nicht fehlschlagen kann, wird Failure als Never angegeben — dies garantiert dem Subscriber, dass onReceive nur mit Output aufgerufen wird. Zu den integrierten Publishern gehören Just (einzelner Wert), Sequence (Array), URLSession.DataTaskPublisher (Netzwerkanfrage), NotificationCenter.Publisher und der @Published-Property-Wrapper.
import Combine
// Publisher aus einer Sequenz erstellen
let publisher = [1, 2, 3, 4, 5].publisher
// Subscriber mit Wertverarbeitung erstellen
class PrintSubscriber: Subscriber {
typealias Input = Int
typealias Failure = Never
func receive(subscription: Subscription) {
subscription.request(.unlimited)
}
func receive(_ input: Int) -> Subscribers.Demand {
print("Received: \(input)")
return .unlimited
}
func receive(completion: Subscribers.Completion<Never>) {
print("Abgeschlossen")
}
}
publisher.subscribe(PrintSubscriber())
Subscription ist ein Protokoll, das die aktive Verbindung zwischen Publisher und Subscriber darstellt. Der Subscriber erhält Subscription in der Methode receive(subscription:) und ruft request(_:) auf, um Demand anzugeben: .unlimited (alle Werte), .max(N) (begrenzte Anzahl) oder .none (Pause). Demand kann sich dynamisch ändern — der Subscriber kann die Anzahl der angeforderten Elemente während des Datenempfangs erhöhen oder verringern. Dies ermöglicht Gegensteuerung ohne Pufferung auf der Publisher-Seite.
Subject ist ein Typ, der Publisher und Subscriber kombiniert. Ein Subject kann als Publisher (Abonnenten abonnieren ihn) und gleichzeitig als Subscriber (Werte werden an ihn gesendet) verwendet werden. Combine bietet zwei Arten von Subject: PassthroughSubject (speichert keinen Zustand, gibt nur neue Werte weiter) und CurrentValueSubject (speichert den aktuellen Wert und gibt ihn an neue Abonnenten weiter). Subject ist notwendig, um imperativen Code in reaktive Combine-Ketten zu integrieren.
let subject = PassthroughSubject<String, Never>()
// Als Publisher abonnieren
let cancellable = subject
.map { $0.uppercased() }
.sink { print($0) }
// Werte als Subscriber senden
subject.send("hello") // Gibt "HELLO" aus
subject.send("world") // Gibt "WORLD" aus
CurrentValueSubject unterscheidet sich von PassthroughSubject durch einen Anfangswert und eine Value-Eigenschaft: Ein Abonnent erhält sofort den aktuellen Wert beim Abonnieren und dann alle nachfolgenden Aktualisierungen. CurrentValueSubject.value ist les- und schreibbar — eine Änderung von value sendet automatisch den neuen Wert an alle Abonnenten. Dies macht CurrentValueSubject zur idealen Wahl für die Zustandsdarstellung in der MVVM-Architektur: ViewModel veröffentlicht CurrentValueSubject, View abonniert Änderungen über sink.
Beide Subjects können den Stream durch Aufruf von send(completion: .finished) oder send(completion: .failure(error)) beenden. Nach der Beendigung nimmt das Subject keine Ereignisse mehr an und sendet keine mehr aus. Für langlebige Streams, die nicht enden sollen (z. B. UI-Ereignisse), wird die Verwendung von PassthroughSubject mit Never Failure empfohlen, um ein versehentliches Aufrufen von send(completion:) zu vermeiden.
Combine-Operatoren sind Publisher-Methoden, die einen neuen Publisher zurückgeben. Jeder Operator erstellt ein neues Objekt, das den vorgelagerten Publisher abonniert und transformierte Werte nachgelagert ausgibt. Da Publisher ein generischer Typ ist, behalten Operatoren eine strenge Typisierung bei: map wandelt Output<A> in Output<B> um, tryMap fügt die Fehlermöglichkeit hinzu. Combine enthält etwa 100 integrierte Operatoren.
| Kategorie | Operator | Zweck |
|---|---|---|
| Transformation | map / tryMap / flatMap | Werte oder Streams transformieren |
| Filterung | filter / compactMap / removeDuplicates | Werte auswählen oder bereinigen |
| Kombination | combineLatest / zip / merge | Mehrere Publisher kombinieren |
| Zeitsteuerung | debounce / throttle / delay | Ereignisse verzögern und ausdünnen |
| Fehlerbehandlung | catch / retry / replaceError | Wiederherstellung nach Failure |
| Demand-Verwaltung | buffer / collect | Gruppierung oder Pufferung |
flatMap in Combine unterscheidet sich wichtig von der RxSwift-Version: Es akzeptiert einen Closure, der einen Publisher mit demselben Failure-Typ zurückgibt, und entfaltet den verschachtelten Publisher in den Hauptstrom. flatMap mit maxPublishers: .max(1) verhält sich wie switchMap — es bricht den vorherigen verschachtelten Publisher ab, wenn ein neuer Wert eintrifft. Dies ist entscheidend für Suchszenarien: Wenn ein neues Zeichen eingegeben wird, wird die vorherige HTTP-Anfrage automatisch abgebrochen.
// Debounce-Suche mit Abbruch der vorherigen Anfrage
searchTextField.textPublisher
.debounce(for: .seconds(0.3), scheduler: RunLoop.main)
.removeDuplicates()
.flatMap(maxPublishers: .max(1)) { query in
apiService.searchPublisher(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.sink { results in
self.tableView.reloadData()
}
.store(in: &cancellables)
Kombinationsoperatoren — combineLatest und zip — funktionieren ähnlich wie in RxSwift: combineLatest gibt ein Tupel der neuesten Werte aller Publisher aus, wenn sich einer von ihnen ändert; zip paart Werte nach Index. merge kombiniert Publisher desselben Typs zu einem Stream, die Reihenfolge wird nicht garantiert. Combine verfügt auch über select — einen seltenen Operator, der den ersten abschließenden Publisher aus mehreren auswählt, und share — Multicasting eines Streams an mehrere Abonnenten ohne erneute Ausführung.
Scheduler in Combine ist ein Protokoll, das den Ausführungskontext für Operatoren definiert. Im Gegensatz zu RxSwift mit seinen über 5 integrierten Schedulern verwendet Combine vorhandene Apple-Mechanismen: DispatchQueue, RunLoop und OperationQueue. Jeder dieser Typen entspricht dem Scheduler-Protokoll, sodass sie direkt an receive(on:) und subscribe(on:) ohne zusätzliche Adapter übergeben werden können.
receive(on:) schaltet den nachgelagerten Bereich auf den angegebenen Scheduler um — entspricht observeOn in RxSwift. Alle Operatoren nach receive(on:) werden auf dem angegebenen Scheduler ausgeführt. subscribe(on:) schaltet den vorgelagerten Bereich um — beeinflusst die Publisher-Ausführung. Ein typisches Muster: subscribe(on: DispatchQueue.global()) für Hintergrundarbeit und receive(on: DispatchQueue.main) für UI-Updates. In SwiftUI ist bei Verwendung von .onReceive keine integrierte Bindung an den Hauptthread erforderlich, aber für sink wird explizites receive(on:).main empfohlen.
// Hintergrundladen + UI im Hauptthread
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background))
.tryMap { data, response -> Data in
guard let http = response as? HTTPURLResponse,
http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return data
}
.receive(on: DispatchQueue.main)
.decode(type: User.self, decoder: JSONDecoder())
.sink(receiveCompletion: { print($0) },
receiveValue: { self.nameLabel.text = $0.name })
.store(in: &cancellables)
RunLoop.main ist eine Alternative zu DispatchQueue.main für UI-Operationen. Der Unterschied besteht darin, dass RunLoop.main an den aktuellen Ereigniszyklus der Anwendung gebunden ist, während DispatchQueue.main an die globale Hauptwarteschlange gebunden ist. Für UIKit wird DispatchQueue.main empfohlen, für SwiftUI — RunLoop.main. ImmediateWhenScheduler führt Operationen synchron im aktuellen Thread aus — standardmäßig für Tests und einfache Publisher verwendet.
ObservableObject ist ein SwiftUI-Protokoll für Objekte, die Änderungen veröffentlichen. Eine Klasse, die ObservableObject implementiert, kann den @Published-Property-Wrapper für Eigenschaften verwenden, deren Änderungen SwiftUI automatisch über die Notwendigkeit einer Neuzeichnung informieren. Unter der Haube erstellt @Published einen Publisher, der den objectWillChange-Publisher benachrichtigt, wenn sich wrappedValue ändert. SwiftUI abonniert objectWillChange über @StateObject, @ObservedObject oder @EnvironmentObject.
@Published ist der häufigste Weg, Combine in SwiftUI zu integrieren. Wenn sich der Wert einer @Published-Eigenschaft ändert, aktualisiert SwiftUI alle Views, die dieses Objekt verwenden. @StateObject erstellt eine ObservableObject-Instanz und abonniert deren Änderungen. Eine mit @StateObject erstellte View wird automatisch neu gezeichnet, wenn sich @Published-Eigenschaften ändern. Wenn das Objekt zwischen mehreren Views geteilt werden muss, werden @ObservedObject oder @EnvironmentObject verwendet.
class UserViewModel: ObservableObject {
@Published var name: String = ""
@Published var age: Int = 0
private var cancellables = Set<AnyCancellable>()
init() {
$name
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.sink { [weak self] newName in
AnalyticsService.logNameChange(newName)
}
.store(in: &cancellables)
}
}
struct UserView: View {
@StateObject var viewModel = UserViewModel()
var body: some View {
TextField("Name", text: $viewModel.name)
}
}
AnyCancellable ist ein Typ-Auslöschungs-Wrapper für Cancellable, der ein Abonnement- Abbruchtoken speichert. Set<AnyCancellable> verwaltet den Lebenszyklus von Abonnements: Wenn der Besitzer deinitialisiert wird, werden alle Cancellable automatisch abgebrochen. In SwiftUI-Projekten wird Set<AnyCancellable> in der ObservableObject-Klasse deklariert, und Abonnements werden über .store(in: &cancellables) hinzugefügt. Für UIKit werden dieselben Mechanismen mit Speicherung in UIViewController über &cancellables oder manuelle cancel()-Aufrufe verwendet.
Combine und RxSwift lösen dieselben reaktiven Programmieraufgaben, haben aber grundlegende architektonische Unterschiede. Combine ist Teil des Apple SDK mit Abwärtskompatibilität bis iOS 13, RxSwift ist eine Drittanbieter-Bibliothek mit Unterstützung für iOS 8+. Combine verwendet strenge Fehlertypisierung durch Failure-Generic, RxSwift verwendet einen einzigen Error-Typ. Combine ist auf Plattformebene in SwiftUI integriert, RxSwift benötigt RxCocoa für UI-Erweiterungen.
Die Wahl zwischen Combine und RxSwift hängt von den Projektanforderungen ab. Wenn die Mindest-iOS-Version >= 13 ist und das Projekt SwiftUI verwendet, ist Combine dank der integrierten Integration und fehlender zusätzlicher Abhängigkeiten die natürliche Wahl. Wenn das Projekt iOS 11-12 unterstützt, eine vorhandene RxSwift-Codebasis enthält oder spezifische Operatoren erfordert, die nur in RxSwift verfügbar sind (z. B. Observable.from(path:)), bleibt RxSwift eine gültige Lösung.
| Eigenschaft | Combine | RxSwift |
|---|---|---|
| Entwickler | Apple (im SDK integriert) | ReactiveX (Community) |
| iOS-Version | iOS 13+ | iOS 8+ |
| Fehlertyp | Generisches Failure (Never für UI) | Error (beliebig) |
| UI-Integration | @Published + SwiftUI | RxCocoa + UIKit |
| Operatoren | ~100 integriert | 400+ Operatoren |
| Swift Concurrency | Über .values (Async-Sequenz) | Über Brückenbibliothek |
Häufig gestellte Fragen
PassthroughSubject speichert keinen Zustand — ein Abonnent erhält nur Ereignisse, die nach dem Abonnement gesendet wurden. CurrentValueSubject speichert den aktuellen Wert und übergibt ihn jedem neuen Abonnenten sofort bei der Anmeldung. CurrentValueSubject eignet sich zur Darstellung von Zuständen (z. B. isLoggedIn).
Ein Abonnement gibt AnyCancellable zurück, das durch Aufruf von cancel() oder bei der Deinitialisierung abgebrochen wird. Für die Gruppenverwaltung verwenden Sie Set<AnyCancellable> — alle Abonnements werden beim Löschen des Sets abgebrochen. Dies ist analog zu DisposeBag in RxSwift.
Ja, Combine bleibt für Streaming-Daten relevant: UI-Ereignisse, debounce, combineLatest, WebSocket. async/await ist praktisch für einmalige Anfragen, Combine — für kontinuierliche oder mehrere Streams. Beide Frameworks ergänzen sich — Publisher kann in AsyncSequence umgewandelt werden.
UIKit hat keine integrierten Publisher, aber Apple bietet Erweiterungen: NotificationCenter.default.publisher(for:), Timer.publish, URLSession.dataTaskPublisher. Für benutzerdefinierte UI-Ereignisse werden PassthroughSubject oder @IBAction verwendet, die über Future oder Subject in einen Publisher eingewickelt werden.
Gegensteuerung (Backpressure) ist ein Mechanismus zur Steuerung der Flussgeschwindigkeit: Der Subscriber teilt dem Publisher über Demand mit, wie viele Elemente er verarbeiten kann. Wenn Demand = .max(1) ist, wartet der Publisher auf eine Anfrage, bevor er den nächsten Wert sendet. Dies verhindert Pufferüberlauf bei nicht übereinstimmenden Geschwindigkeiten von Produzent und Konsument.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch