Einführung in Reactive Programming mit dem Combine Framework

von @ralfebert · aktualisiert am 28. Juli 2021

Mit dem Combine Framework steht nun ein natives iOS-Framework zur reaktiven Programmierung zur Verfügung. Dies ist eine Abstraktion, die auf der Bereitstellung von Werten und der Verknüpfung von Werten zu „Werteflüssen“ basiert.

Das elementarste Konzept in Combine sind Publisher und Subscriber. Publisher veröffentlichen Werte, die von Subscribern abonniert werden.

Auf dieser Grundlage können die Werte mit einer Vielzahl von Operatoren, wie zum Beispiel map, verarbeitet werden. Damit wird ein Konstrukt aufgebaut, das man sich wie ein „Rohrsystem“ vorstellen kann, durch das die Werte fließen. Am Ende stehen dabei typischerweise Senken (sink), die die ermittelten Werte final verwenden.

Das folgende Beispielprojekt Clock illustriert die grundlegenden Konzepte am Beispiel der Anzeige der Systemzeit:

Beispielprojekt: Clock

Dabei wird mittels der Combine-Integration in Foundation ein Publisher erstellt, der die aktuelle Systemzeit veröffentlicht. Diese wird mit map verarbeitet und mit sink in einem UILabel angezeigt:

Combine Beispiel: Systemtime Clock

Beispielprojekt: Clock

  1. Lade den Start-Stand von dem Beispielprojekt Clock und öffne den TimeViewController.

  2. Implementiere die Funktion systemTimePublisher so, das ein Timer-Publisher erstellt wird mittels Timer.publish. Dieser muss noch aktiviert werden, dies geschieht durch den Aufruf von autoconnect(). Rufe eraseToAnyPublisher auf, um die Typsignatur zu AnyPublisher<Date, Never> zu bereinigen (ein Publisher, der ein Datumswert veröffentlicht und keinen Fehlerfall kennt):

    func systemTimePublisher() -> AnyPublisher<Date, Never> {
        Timer.publish(every: TimeInterval(1), on: .main, in: .default)
        .autoconnect()
        .eraseToAnyPublisher()
    }
    
  3. Verwende in viewWillAppear die Funktion systemTimePublisher, formatiere den Datumswert mit map (ein DateFormatter ist im Projekt bereits vorbereitet: DateFormats.timeOnlyFormatter):

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    
        systemTimePublisher()
        .map { date in DateFormats.timeOnlyFormatter.string(from: date) }
    }
    
  4. Verwende die Methode sink um den publizierten Wert in dem timeLabel anzuzeigen:

    systemTimePublisher()
        .map { date in DateFormats.timeOnlyFormatter.string(from: date) }
        .sink { (text) in self?.timeLabel.text = text }
    
  5. Starte die App und prüfe, ob die Zeit angezeigt wird - dies wird nicht der Fall sein, da der Publisher zwar abonniert wird, dieses Abonnement aber nirgendwo gehalten wird und damit sofort wieder hinfällig wird.

  6. Es fehlt noch das Halten des Abonnement und das Lösen, wenn dieser nicht mehr benötigt wird, z.B. wenn der Controller gerade nicht sichtbar ist. Deklariere dazu ein Set<AnyCancellable> um mit der store-Methode aufgebaute Verbindungen abzulegen. Definiere zudem die self-Referenz im sink-Block als weak, damit kein Speicherverwaltungszyklus entsteht:

    class TimeViewController: UIViewController {
    
        // ...
    
        var subscriptions = Set<AnyCancellable>()
    
        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
    
            systemTimePublisher()
                .map { /* ... */ }
                .sink { [weak self] (text) in self?.timeLabel.text = text }
                .store(in: &self.subscriptions)
        }
    
        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            self.subscriptions.removeAll()
        }
    
    }
    
  7. Teste das die Zeit nun korrekt angezeigt und aktualisiert wird.

  8. Prüfe mit einem debugPrint im sink-Block, das der Datumswert nicht mehr aktualisiert wird, wenn der Controller nicht sichtbar ist.

Weitere Informationen