Speichern der Geschäftslogik in Swift Combine. Teil 1

Datenorientiertes Kombinieren





Die Übersetzung des Artikels wurde speziell für Studenten des Fortgeschrittenenkurses "iOS Developer" vorbereitet .





In der vorherigen Reihe von Beiträgen haben wir erfolgreich eine Plattform auf SwiftUI aufgebaut, mit der Sie die Reihenfolge der Werte, die durch den Publisher Combine gehen, frei beobachten können .

Wir haben auch eine Reihe von Beispielen demonstriert mehrere Standardoperatoren kombinieren, wie zum Modifizieren und Umwandeln von Werten in der Lage sind , Sequenzen , filterwie map, dropund scan. Darüber hinaus haben wir einige Operatoren vorgestellt, die die Sequenz verbinden ( Zipund CombineLatest) oder vereinheitlichen ( Mergeund Append).

An diesem Punkt könnten einige von Ihnen es leid sein, so viel Code für jedes Beispiel organisieren und pflegen zu müssen (zumindest bin ich schon müde). Sehen Sie, wie viele davon sich im Kombinations-Magic-Swiftui-Repository im Tutorial-Ordner befinden? Jedes der Beispiele ist eine Darstellung von SwiftUI. Jeder von ihnen überträgt einfach einen oder mehrere Verlage an StreamViewund StreamViewsigniert Verlage auf Knopfdruck.

Daher sollte ich in der Lage sein, beim Starten und Wiederverwenden der Anwendung programmgesteuert eine Liste der Herausgeber zu erstellen StreamView, wie im folgenden Screenshot dargestellt.



Das Problem bei dieser Lösung ist jedoch die Skalierbarkeit, wenn Sie viele Publisher erstellen müssen.

Meine Lösung für dieses Problem besteht darin, diese Herausgeber irgendwie zu speichern. Wenn ich sie irgendwie serialisieren kann, kann ich sie speichern. Wenn es mir gelingt, sie zu speichern, kann ich sie nicht nur ändern, ohne den Code zu ändern, sondern sie auch für andere Geräte freigeben, die Combine unterstützen .

Lagerung und Transfer von Betreibern kombinieren


Schauen wir uns nun unsere Ziele genauer an. Da wir eine Liste von Streams und Operatoren in einem Format haben Publisher, möchten wir sie in jeder Art von Speicher speichern können - zum Beispiel auf einer Festplatte oder in einer Datenbank.

Natürlich müssen wir auch in der Lage sein, die gespeicherten Daten zurück in den Herausgeber zu konvertieren, aber zusätzlich möchten wir in der Lage sein, diese Herausgeber mit Betreibern von einem Ort zum anderen auszutauschen, zu übertragen und zu verteilen.

Nachdem wir eine solche Struktur in einer verteilten Umgebung eingerichtet haben, wie Sie vielleicht vermutet haben, kann ein zentraler Dienst beginnen, die Rechenlogik für eine Gruppe von Clients zu verwalten.



Codierbare Struktur


Wie machen wir das? Wir werden zunächst eine Struktur entwickeln, die serialisierbar und deserialisierbar ist. Das Swift-Protokoll Codable ermöglicht es uns, dies durch JSONEncoderund zu tun JSONDecoder. Darüber hinaus muss die Struktur Daten und Verhaltensweisen für die kleinste Werteinheit im Stream bis hin zu komplexen Operatorketten korrekt darstellen.

Bevor wir verstehen, welche Komponenten für die Strukturen erforderlich sind, die wir erstellen werden, erinnern wir uns an den Hauptstrom, den wir in der vorherigen Reihe von Beiträgen erstellt haben .

Strom von Zahlen




Dies ist der einfachste Stream. Wenn Sie jedoch genauer hinschauen, werden Sie feststellen, dass dies nicht nur eine Folge von Arrays ist. Jeder der runden Blöcke hat einen eigenen Verzögerungsoperator ( delay), der die tatsächliche Zeit bestimmt, zu der er gesendet werden soll. Jeder Wert in Combine sieht folgendermaßen aus:

Just(value).delay(for: .seconds(1), scheduler: DispatchQueue.main)

Und im Allgemeinen sieht alles so aus:

let val1 = Just(1).delay(for: .seconds(1), scheduler:   DispatchQueue.main)
let val2 = Just(2).delay(for: .seconds(1), scheduler: DispatchQueue.main)
let val3 = ....
let val4 = ....
let publisher = val1.append(val2).append(val3).append(val4)

Jeder Wert wird um eine Sekunde verzögert, und dieselbe Anweisung wird zum nächsten Wert hinzugefügt delay.

Daher lernen wir aus unseren Beobachtungen zwei Dinge.

  1. Ein Stream ist nicht die kleinste Einheit in einer Struktur. Der kleinste ist der Wert des Streams.
  2. Jeder Stream-Wert kann unbegrenzte Operatoren haben, die steuern, wann und welcher Wert übertragen wird.

Erstellen Sie Ihr StreamItem


Da der Wert des Streams und seiner Operatoren die kleinste Einheit ist, erstellen wir zunächst seine Struktur. Nennen wir sie StreamItem.

struct StreamItem<T: Codable>: Codable {
 let value: T
 var operators: [Operator]
}

StreamItemEnthält den Wert des Streams und ein Array von Operatoren. Entsprechend unseren Anforderungen möchten wir in der Lage sein, alles in der Struktur so zu erhalten, dass beides valueund StreamItemdas Protokoll eingehalten werden Codable.

Der Stream-Wert muss universell sein, um jeden Werttyp aufnehmen zu können.

Erstellen Sie Ihr StreamModel


Wir werden die Struktur für die Operatoren später diskutieren. Verbinden wir das Array StreamItemmit StreamModel.

struct StreamModel<T: Codable>: Codable, Identifiable {
 var id: UUID
 var name: String?
 var description: String?
 var stream: [StreamItem<T>]
}

StreamModelenthält ein Array von StreamItems. StreamModelhat auch Eigenschaften für Bezeichner, Name und Beschreibung. Auch hier sollte alles in StreamModelfür die Speicherung und Verteilung codierbar sein.

Erstellen Sie eine Operatorstruktur


Wie bereits erwähnt, können Bediener delaydie Übertragungszeit ändern StreamItem.

enum Operator {
 case delay(seconds: Double)
}

Wir betrachten den Operator delayals eine Aufzählung ( enum) mit einem zugeordneten Wert, um die Verzögerungszeit zu speichern.

Natürlich Operatormuss auch die Aufzählung übereinstimmen Codable, einschließlich des Codierens und Decodierens verwandter Werte. Siehe vollständige Implementierung unten.

enum Operator {
    case delay(seconds: Double)
}

extension Operator: Codable {

    enum CodingKeys: CodingKey {
        case delay
    }

    struct DelayParameters: Codable {
        let seconds: Double
    }

    enum CodingError: Error { case decoding(String) }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let delayParameters = try? container.decodeIfPresent(DelayParameters.self, forKey: .delay) {
            self = .delay(seconds: delayParameters.seconds)
            return
        }
        throw CodingError.decoding("Decoding Failed. \(dump(container))")
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .delay(let seconds):
            try container.encode(DelayParameters(seconds: seconds), forKey: .delay)
        }
    }

}

Wir haben jetzt eine gute Struktur, um diesen sequentiellen Strom darzustellen, der mit einem zweiten Verzögerungsintervall Werte von 1 bis 4 erzeugt.

l
et streamA = (1...4).map { StreamItem(value: $0,
operators: [.delay(seconds: 1)]) }
let serialStreamA = StreamModel(id: UUID(), name: "Serial Stream A",
description: nil, stream: streamA)

Konvertieren Sie StreamModel in Publisher


Jetzt haben wir eine Instanz des Streams erstellt. Wenn wir es jedoch nicht in einen Verlag umwandeln, ist alles bedeutungslos. Lass es uns versuchen.

Zunächst bezieht sich jedes Operatormodell auf den tatsächlichen Kombinationsoperator, der diesem Herausgeber hinzugefügt und an den betriebenen Herausgeber zurückgegeben werden sollte.

extension Operator {
func applyPublisher<T>(_ publisher: AnyPublisher<T, Never>) -> AnyPublisher<T, Never> {
  switch self {
    case .delay(let seconds):
    return publisher.delay(for: .seconds(seconds), scheduler: DispatchQueue.main).eraseToAnyPublisher()
  }
 }
}

Im Moment gibt es nur einen Operatortyp - delay. Wir werden im Laufe der Zeit weitere hinzufügen.

Jetzt können wir Publisher für alle nutzen StreamItem.

extension StreamItem {
 func toPublisher() -> AnyPublisher<T, Never> {
   var publisher: AnyPublisher<T, Never> =
                  Just(value).eraseToAnyPublisher()
   self.operators.forEach {
      publisher = $0.applyPublisher(publisher)
   }
  return publisher
}
}

Wir beginnen mit dem Wert Just, verallgemeinern ihn mit der Methode eraseToAnyPublisherund verwenden dann die Herausgeber aller verwandten Operatoren.

Auf der Ebene erhalten StreamModelwir den Herausgeber des gesamten Streams.

extension StreamModel {
 func toPublisher() -> AnyPublisher<T, Never> {
   let intervalPublishers =
        self.stream.map { $0.toPublisher() }
   var publisher: AnyPublisher<T, Never>?
   for intervalPublisher in intervalPublishers {
     if publisher == nil {
       publisher = intervalPublisher
       continue
     }
     publisher =
        publisher?.append(intervalPublisher).eraseToAnyPublisher()
   }
   return publisher ?? Empty().eraseToAnyPublisher()
 }
}

Sie haben es richtig erraten: Wir verwenden die Methode append, um Verlage zu kombinieren.

Visualisierung, Bearbeitung und erneut Visualisierung eines Streams


Jetzt können wir einfach den Publisher dekodieren, eine StreamView übertragen und erstellen (siehe, wie wir es in früheren Beiträgen gemacht haben ). Und zu guter Letzt: Jetzt können wir einfach bearbeiten StreamModel, weitere StreamItemmit neuen Werten hinzufügen und dieses Modell sogar über das Internet mit anderen Geräten teilen.

Siehe die Demo unten. Jetzt können wir Änderungen am Stream vornehmen, ohne den Code zu ändern.



Nächstes Kapitel: Serialisieren / Deserialisieren von Filtern und Kartenoperatoren


Im nächsten Teil werden wir der Operatoraufzählung weitere Operatoren hinzufügen und diese auf Thread-Ebene anwenden.

Bis zum nächsten Mal finden Sie den Quellcode hier in diesem Combine-Magic-Swifui-Repository im Ordner Combine-Playground.

Wir warten auf Ihre Kommentare und sind zu einem offenen Webinar zum Thema "iOS-Anwendung auf SwiftUI mit Kotlin Mobile Multiplatform" eingeladen .

Source: https://habr.com/ru/post/undefined/


All Articles