Einführung in die reaktive Programmierung mit Combine in iOS

Moderne iOS-Anwendungen sind von Natur aus asynchron. Von Netzwerkanfragen und Datenbankanfragen bis hin zu Benutzeroberflächenereignissen und Sensordaten müssen Entwickler einen konstanten Informationsfluss verwalten, der zu unvorhersehbaren Zeiten eintrifft. Traditionell erforderte der Umgang mit diesen Ereignissen Delegierte, Rückrufe oder Benachrichtigungszentren, was oft zu komplexem, fehleranfälligem Code führte. Das von Apple in iOS 13 eingeführte Combine-Framework bietet einen einheitlichen, deklarativen Ansatz für reaktive Programmierung, der den Umgang mit asynchronen Ereignissen und Datenströmen vereinfacht. Durch die Verwendung von Publishern, Abonnenten und Betreibern ermöglicht Combine Ihnen, saubere, zusammensetzbare Pipelines zu schreiben, die effizient auf Änderungen reagieren. Dieser Artikel bietet eine gründliche, praktische Anleitung zur Verwendung von Combine für reaktive Programmierung in iOS, von Grundlagen bis hin zu erweiterten Mustern.

Was ist das Combine Framework?

Combine ist ein natives Swift-Framework, das eine deklarative API für die Verarbeitung von Werten im Laufe der Zeit bereitstellt. Es ist Teil des Apple-Ökosystems und lässt sich nahtlos in SwiftUI, Foundation und UIKit integrieren. Combine basiert auf drei Kernabstraktionen: publisher emittieren Werte, Abonnenten empfangen und verarbeiten diese Werte und Betreiber transformieren oder filtern den Wertestrom. Zusammen erstellen sie eine Pipeline, die definiert, wie Daten durch Ihre Anwendung fließen.

Im Gegensatz zu früheren Mustern wie oder KVO bietet Combine Sicherheit für die Compiler-Zeit, Typschluss und leistungsstarke Komposition. Es ähnelt im Konzept reaktiven Erweiterungen wie RxSwift, ist aber First-Party und tief in Apples Plattformen integriert. Combine ist für moderne iOS-Entwickler unerlässlich, insbesondere für diejenigen, die Apps mit SwiftUI erstellen, wo Combine viele datengesteuerte Funktionen unterstützt.

Kernkonzepte von Combine

Bevor Sie in den Code eintauchen, ist es wichtig, die vier Hauptkomponenten zu erfassen, die jede Combine-Pipeline bilden:

  • Publisher – Ein Typ, der eine Sequenz von Werten im Laufe der Zeit ausgeben kann. Er erklärt, welche Art von Werten er ausgibt () und welche Art von Fehlern er mit () ausfallen kann. Ein Publisher kann eine beliebige Anzahl von Werten ausgeben, dann schließlich abschließen oder ausfallen.
  • Subscriber – Ein Typ, der Werte von einem Publisher erhält. Er definiert drei Methoden: , (um einen neuen Wert zu verarbeiten) und (um Erfolg oder Misserfolg zu bewältigen).
  • ]Operator – Ein Publisher, der Werte von einem Upstream-Publisher transformiert. Operatoren wie , , und sind selbst Publisher, die Upstream-Publisher abonnieren, eine Transformation anwenden und das Ergebnis erneut veröffentlichen. Diese Verkettung macht Combine-Pipelines leistungsfähig.
  • Subscription – Die Verbindung zwischen einem Publisher und einem Abonnenten. Ein Abonnement steuert den Nachfragefluss (Back Pressure). Abonnements können gekündigt werden, um den Empfang von Werten zu stoppen, was automatisch gehandhabt wird, wenn ein dealisiert oder manuell gekündigt wird.

Zusätzlich stellt Combine das Kündbare Protokoll (und den konkreten Typ ) zur Verfügung, um den Lebenszyklus eines Abonnements zu verwalten. Das Speichern von Cancellables in einem Set (oft ) stellt sicher, dass Abonnements automatisch heruntergerissen werden, wenn das besitzende Objekt deloziert wird.

Publisher erstellen

Combine bietet viele integrierte Publisher, die gängige asynchrone Aufgaben abdecken.Sie können auch benutzerdefinierte Publisher erstellen, indem Sie das -Protokoll implementieren oder Convenience-Initializer verwenden.

Timer Publisher

Verwenden Sie , um das aktuelle Datum in regelmäßigen Abständen auszusenden.

let timerPublisher = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()

Der Herausgeber gibt Werte jede Sekunde auf der Hauptlaufschleife aus.

URLSessions Data Task Publisher

Kombinieren Sie extends mit einem Publisher, der Netzwerkanforderungen ausführt:

guard let url = URL(string: "https://api.example.com/data") else { return }
let publisher = URLSession.shared.dataTaskPublisher(for: url)

Dieser Verlag gibt ein Tupel nach Abschluss oder ein nach dem Scheitern.

NotificationCenter Publisher

Konvertieren Sie eine Benachrichtigung in einen Publisher:

let notificationPublisher = NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)

Nützlich für die Reaktion auf Systemereignisse ohne manuelles Hinzufügen von Beobachtern.

Gerecht und Zukunft

gibt einen einzelnen Wert aus und vervollständigt ihn sofort.

let justPublisher = Just("Hello, Combine!")

ist ein Publisher, der schließlich einen einzelnen Wert asynchron erzeugt (oder ausfällt), der die bereitgestellte Schließung genau einmal ausführt und das Ergebnis für spätere Abonnenten zwischenspeichert:

let futurePublisher = Future<String, Error> { promise in
 // Perform async work, then call promise(.success(value)) or promise(.failure(error))
}

PassthroughSubject und CurrentValueSubject

Probanden fungieren sowohl als Publisher als auch als Abonnent, so dass Sie Werte manuell in einen Stream einspeisen können. sendet Werte an Abonnenten, ohne einen aktuellen Wert beizubehalten; behält den neuesten Wert und gibt ihn neuen Abonnenten wieder:

let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)

let currentValueSubject = CurrentValueSubject<String, Never>("initial")
currentValueSubject.send("updated")
print(currentValueSubject.value) // "updated"

Abonnieren eines Publishers

Um eine Pipeline zu aktivieren, benötigen Sie einen Abonnenten. Die am häufigsten verwendeten sind und .

Der Sink Subscriber

bietet eine schließungsbasierte Handhabung für empfangene Werte und Abschlussereignisse:

let subscription = timerPublisher.sink { time in
 print("Timer fired at \(time)")
}

Sie können auch Fehler behandeln, indem Sie eine -Schließung bereitstellen:

publisher
 .sink(receiveCompletion: { completion in
 switch completion {
 case .finished:
 print("Stream finished")
 case .failure(let error):
 print("Error: \(error)")
 }
 }, receiveValue: { value in
 print("Received: \(value)")
 })

Der Assign-Abonnent

bindet die Ausgabe des Herausgebers an eine Eigenschaft eines Objekts, optional über einen Schlüsselpfad.

class ViewModel {
 @Published var text: String = ""
}

let viewModel = ViewModel()
let publisher = Just("Hello")
let cancellable = publisher.assign(to: \.text, on: viewModel)
print(viewModel.text) // "Hello"

Kundenspezifischer Abonnent

Für fortgeschrittene Anwendungsfälle können Sie das -Protokoll direkt implementieren, dies ist jedoch selten notwendig; und decken die meisten Szenarien ab.

Daten mit Operatoren transformieren

Betreiber sind das Rückgrat von Combine, so dass Sie komplexe Pipelines aus einfachen Bausteinen zusammenstellen können.

Karte

Transformiert jeden emittierten Wert mit einer Schließung:

publisher
 .map { $0 * 2 }
 .sink { print($0) }

Filter

Durchläuft Werte, die ein Prädikat erfüllen:

publisher
 .filter { $0.isMultiple(of: 2) }
 .sink { print("Even: \($0)") }

FlatMap

Verflachen mehrerer interner Publisher in einem einzigen Stream. Wird üblicherweise verwendet, wenn jeder Wert eine neue asynchrone Operation auslöst:

let searchPublisher = searchTextPublisher
 .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
 .removeDuplicates()
 .flatMap { text in
 return api.searchPublisher(for: text)
 }

CombineLatest

Kombiniert die neuesten Werte von zwei oder mehr Publishern zu einem Tupel. Es gibt einen neuen Wert aus, wenn einer der Publisher Folgendes ausgibt:

Publishers.CombineLatest(usernamePublisher, passwordPublisher)
 .map { username, password in
 return username.count > 0 && password.count > 0
 }
 .assign(to: \.isEnabled, on: loginButton)

Zip

Pairs Werte von mehreren Publishern in der Reihenfolge, warten auf alle Publisher, um den nächsten Wert zu emittieren, bevor das Paar ausgibt:

Publishers.Zip(emailsPublisher, namesPublisher)
 .sink { email, name in
 print("\(name): \(email)")
 }

Fusion

Interleaves Werte von mehreren Publishern des gleichen Typs in einem einzigen Stream:

Publishers.Merge(leftPublisher, rightPublisher)
 .sink { print($0) }

Sammeln

Puffert Werte in Arrays und sendet, sobald der Puffer gefüllt ist oder der Stream abgeschlossen ist:

publisher
 .collect(3) // emit arrays of 3 values
 .sink { print($0) }

Debounce und Throttle

wartet auf eine bestimmte Pause zwischen den Werten, bevor sie die letzte aussendet. gibt höchstens einen Wert pro Zeitintervall aus.

textField.textPublisher
 .debounce(for: .seconds(0.3), scheduler: RunLoop.main)
 .sink { text in
 // Perform search after user stops typing
 }

Fehlerbehandlung in Kombination

Die eines Publishers definiert, welche Fehler er aussenden kann. Wenn ein Publisher ausfällt, wird das Abonnement beendet. Verwenden Sie Fehlerbehandlungsoperatoren, um Fehler wiederherzustellen oder zu transformieren.

tryMap und tryFilter

Wenn Ihre Transformationsschließung werfen kann, verwenden Sie oder .

publisher
 .tryMap { value in
 guard value > 0 else { throw InputError.invalid }
 return value * 2
 }

Fang

Ersetzen Sie einen Fehler durch einen alternativen Herausgeber.

publisher
 .catch { error in
 return Just(0) // fallback value
 }

Wiederholen

Registriert automatisch eine bestimmte Anzahl von Malen an den Upstream-Publisher, wenn er fehlschlägt:

publisher
 .retry(3)
 .sink(receiveCompletion: { ... }, receiveValue: { ... })

ErsetzenError und ErsetzenEmpty

ersetzt einen Fehler durch einen bestimmten Wert. stellt einen Wert bereit, wenn der Herausgeber ohne Ausstrahlung etwas abschließt.

Speicherverwaltung und -abbruch

Jedes Abonnement verbraucht Ressourcen und muss ordnungsgemäß gekündigt werden, um Speicherlecks zu vermeiden. Combine stellt , eine typgelöschte, stornierbare Referenz bereit.

Speichern Sie Cancellables in einer Sammlung, typischerweise ein , das deallokalisiert wird, wenn das besitzende Objekt (z. B. ein View-Controller) Deinitialisiert wird.

private var cancellables = Set<AnyCancellable>()

func setupBindings() {
 publisher
 .sink { [weak self] value in
 self?.updateUI(value)
 }
 .store(in: &cancellables)
}

Sie können auch manuell auf einer abschaltbaren Datei aufrufen, die das Abonnement beendet und Ressourcen freigibt.

Vollständiges praktisches Beispiel: Netzwerkanforderung mit UI-Updates

Lassen Sie uns ein reales Szenario erstellen: Benutzerdaten aus einer API abrufen, dekodieren und eine SwiftUI-Ansicht aktualisieren. Dieses Beispiel zeigt Fehlerbehandlung, Operatoren und Thread-Management.

import SwiftUI
import Combine

struct User: Decodable, Identifiable {
 let id: Int
 let name: String
 let email: String
}

class UserViewModel: ObservableObject {
 @Published var users: [User] = []
 @Published var isLoading = false
 @Published var errorMessage: String? = nil

 private var cancellables = Set<AnyCancellable>()

 func fetchUsers() {
 isLoading = true
 errorMessage = nil

 guard let url = URL(string: "https://jsonplaceholder.typicode.com/users") else {
 errorMessage = "Invalid URL"
 isLoading = false
 return
 }

 URLSession.shared.dataTaskPublisher(for: url)
 .map { $0.data }
 .decode(type: [User].self, decoder: JSONDecoder())
 .receive(on: DispatchQueue.main)
 .sink(receiveCompletion: { [weak self] completion in
 self?.isLoading = false
 switch completion {
 case .finished:
 break
 case .failure(let error):
 self?.errorMessage = error.localizedDescription
 }
 }, receiveValue: { [weak self] users in
 self?.users = users
 })
 .store(in: &cancellables)
 }
}

Schlüsselpunkte:

  • extrahiert die Rohdaten aus dem Antworttupel.
  • verwendet den eingebauten Operator von Combine, um JSON zu dekodieren.
  • stellt sicher, dass UI-Updates im Hauptthread stattfinden.
  • Das Speichern des Abonnements in bindet seinen Lebenszyklus an das Ansichtsmodell.
  • Der Fehler wird über die Eigenschaft FLT: 71 aufgetaucht, die an eine Warnung in SwiftUI gebunden werden kann.

Kombinieren und SwiftUI: Eine natürliche Partnerschaft

Die reaktive Architektur von SwiftUI basiert stark auf Combine. Eigenschaften, die mit kommentiert wurden, erstellen Publisher, die SwiftUI automatisch benachrichtigen, wenn sie sich ändern. Kombinieren Sie Operatoren, die es Ihnen ermöglichen, asynchrone Daten zu transformieren, bevor sie die Ansicht erreichen. Zum Beispiel können Sie die Eingabe eines Textfeldes debouncen, dann Suchergebnisse abrufen und die Benutzeroberfläche mithilfe von Combines oder aktualisieren.

Die Verwendung von Combine with SwiftUI eliminiert die Notwendigkeit manueller KVO- oder Delegierungsmuster, was zu einem vorhersagbareren und testbareren Code führt.

Kombinieren vs. Andere reaktive Frameworks

Vor Combine verwendeten iOS-Entwickler häufig Frameworks von Drittanbietern wie RxSwift oder ReactiveCocoa. Combine bietet mehrere Vorteile: First-Party-Integration, kein Abhängigkeitsmanagement, nahtlose Interoperabilität mit SwiftUI und Foundation und starke Typensicherheit. RxSwift hat jedoch eine größere Anzahl von Operatoren und ein ausgereifteres Ökosystem. Wenn Sie ein neues Projekt starten, das auf iOS 13+ abzielt, ist Combine die empfohlene Wahl. Für Projekte, die ältere Betriebssystemversionen unterstützen, bleibt RxSwift eine praktikable Option.

Best Practices für die Verwendung von Combine

  • Behalte Pipelines einfach – Komplexe Ketten können schwierig zu debuggen sein.
  • Verwalte Abonnements sorgfältig – Speichere immer ausfallende Objekte und vermeide Zyklen, indem du in Schließungen verwendest, die erfassen.
  • Verwende geeignete Scheduler – Verwende , um in die Hauptwarteschlange für UI-Updates zu wechseln, und , um schwere Arbeit an Hintergrundwarteschlangen auszuführen.
  • Bevorzugen für einfache Eigenschaftsbindungen – Es reduziert den Boilerplate-Code.
  • Testen Sie Ihre Pipelines – Kombinieren Sie die Testbarkeit hervorragend, weil Sie Publisher mit oder erstellen und die Ausgaben anhand der Erwartungen überprüfen können.
  • Vor dem Gegendruck – Das bedarfsgesteuerte System von Combine sorgt dafür, dass die Teilnehmer den Fluss steuern. Operatoren wie oder können helfen, wenn Daten schneller ankommen, als sie verarbeitet werden können.

Schlussfolgerung

Das Combine-Framework revolutioniert, wie iOS-Entwickler mit asynchronen Ereignissen und Datenströmen umgehen. Durch das Verständnis von Publishern, Abonnenten und Betreibern können Sie prägnante, reaktive Pipelines erstellen, die einfacher zu begründen und zu pflegen sind. Beginnen Sie mit einfachen Beispielen wie dem Reagieren auf Benachrichtigungen oder dem Abrufen von Netzwerkdaten, dann erkunden Sie schrittweise fortgeschrittene Operator wie , und Fehlerwiederherstellungsmuster. Combine ist ein Eckpfeiler der modernen iOS-Entwicklung, insbesondere wenn es mit SwiftUI gepaart wird. Beherrschen Sie es wird Sie zu einem effektiveren Entwickler und ermöglichen Sie, Apps zu erstellen, die fließend auf Benutzerinteraktionen und externe Datenquellen reagieren.

For further reading, consult the official Apple Combine documentation, Hacking with Swift’s Combine introduction, or Ray Wenderlich’s Combine book for deeper dives.