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 , filter
wie map
, drop
und scan
. Darüber hinaus haben wir einige Operatoren vorgestellt, die die Sequenz verbinden ( Zip
und CombineLatest
) oder vereinheitlichen ( Merge
und 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 StreamView
und StreamView
signiert 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 JSONEncoder
und 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.- Ein Stream ist nicht die kleinste Einheit in einer Struktur. Der kleinste ist der Wert des Streams.
- 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]
}
StreamItem
Enthä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 value
und StreamItem
das 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 StreamItem
mit StreamModel
.struct StreamModel<T: Codable>: Codable, Identifiable {
var id: UUID
var name: String?
var description: String?
var stream: [StreamItem<T>]
}
StreamModel
enthält ein Array von StreamItem
s. StreamModel
hat auch Eigenschaften für Bezeichner, Name und Beschreibung. Auch hier sollte alles in StreamModel
für die Speicherung und Verteilung codierbar sein.Erstellen Sie eine Operatorstruktur
Wie bereits erwähnt, können Bediener delay
die Übertragungszeit ändern StreamItem
.enum Operator {
case delay(seconds: Double)
}
Wir betrachten den Operator delay
als eine Aufzählung ( enum
) mit einem zugeordneten Wert, um die Verzögerungszeit zu speichern.Natürlich Operator
muss 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.let 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 eraseToAnyPublisher
und verwenden dann die Herausgeber aller verwandten Operatoren.Auf der Ebene erhalten StreamModel
wir 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 StreamItem
mit 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 .