Combiné orienté données
La traduction de l'article a été préparée spécialement pour les étudiants du cours avancé "iOS Developer" .
Dans la série de publications précédente , nous avons construit avec succès une plate-forme au-dessus de SwiftUI, avec laquelle vous pouvez observer librement la séquence de valeurs passant par l' éditeur Combine.Nous avons également créé une série d'exemples illustrant plusieurs opérateurs par défaut Combine capables de modifier et de convertir des valeurs dans des séquences telles filter
que map
, drop
et scan
. De plus, nous avons présenté quelques opérateurs qui connectent ( Zip
et CombineLatest
) ou unifient ( Merge
et Append
) la séquence.À ce stade, certains d'entre vous pourraient être fatigués d'avoir à organiser et à maintenir autant de code pour chaque exemple (au moins, je suis déjà fatigué). Voir combien d'entre eux sont dans le référentiel combine-magic-swiftui dans le dossier tutoriel? Chacun des exemples est une représentation de SwiftUI. Chacun d'eux transfère simplement un ou plusieurs éditeurs vers StreamView
et StreamView
signe les éditeurs en cliquant sur un bouton.Par conséquent, je devrais être en mesure de générer par programmation une liste d'éditeurs au démarrage et à la réutilisation de l'application StreamView
, comme dans la capture d'écran ci-dessous.
Cependant, le problème avec cette solution est l'évolutivité, lorsque vous devez créer de nombreux éditeurs.Ma solution à ce problème consiste en quelque sorte à stocker ces éditeurs. Si je peux les sérialiser d'une manière ou d'une autre, je peux les enregistrer. Si je parviens à les enregistrer, je peux non seulement les modifier sans changer le code, mais je peux également les partager avec d'autres appareils qui prennent en charge Combine .Stockage et transfert d'opérateurs Combiner
Voyons maintenant plus précisément nos objectifs. Puisque nous avons une liste de flux et d'opérateurs dans un format Publisher
, nous aimerions pouvoir les enregistrer dans n'importe quel type de stockage - par exemple, sur un disque dur ou dans une base de données.Évidemment, nous devons également pouvoir reconvertir les données stockées vers l'éditeur, mais en plus, nous voulons pouvoir échanger, transférer et distribuer ces éditeurs avec des opérateurs d'un endroit à un autre.Après avoir mis en place une telle structure, comme vous l'avez peut-être deviné, dans un environnement distribué, un service centralisé peut commencer à gérer la logique de calcul pour un groupe de clients.
Structure codable
Alors, comment faisons-nous cela? Nous commencerons par développer une structure sérialisable et désérialisable. Le protocole Swift Codable
nous permet de le faire via JSONEncoder
et JSONDecoder
. De plus, la structure doit représenter correctement les données et les comportements pour la plus petite unité de valeur du flux, jusqu'aux chaînes d'opérateurs complexes.Avant de continuer pour comprendre quels composants sont nécessaires pour les structures que nous allons créer, rappelons le flux principal que nous avons créé dans la série de publications précédente .Flux de nombres
C'est le flux le plus simple; cependant, si vous regardez plus en profondeur, vous remarquerez qu'il ne s'agit pas simplement d'une séquence de tableaux. Chacun des blocs ronds a son propre opérateur de retard ( delay
), qui détermine l'heure réelle à laquelle il doit être transmis. Chaque valeur dans Combine ressemble à ceci:Just(value).delay(for: .seconds(1), scheduler: DispatchQueue.main)
Et en général, tout cela ressemble à ceci: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)
Chaque valeur est retardée d'une seconde et la même instruction est ajoutée à la valeur suivante delay
.Par conséquent, nous apprenons deux choses de nos observations.- Un flux n'est pas la plus petite unité d'une structure. Le plus petit est la valeur du flux.
- Chaque valeur de flux peut avoir un nombre illimité d'opérateurs qui contrôlent quand et quelle valeur est transmise.
Créez votre StreamItem
La valeur du flux et de ses opérateurs étant la plus petite unité, nous commençons par créer sa structure. Appelons-la StreamItem
.struct StreamItem<T: Codable>: Codable {
let value: T
var operators: [Operator]
}
StreamItem
inclut la valeur du flux et un tableau d'opérateurs. Selon nos exigences, nous voulons pouvoir tout conserver dans la structure pour que les deux value
, et se StreamItem
conformer au protocole Codable
.La valeur du flux doit être universelle pour s'adapter à tout type de valeur.Créez votre StreamModel
Nous discuterons plus tard de la structure des opérateurs. Connectons le tableau StreamItem
à StreamModel
.struct StreamModel<T: Codable>: Codable, Identifiable {
var id: UUID
var name: String?
var description: String?
var stream: [StreamItem<T>]
}
StreamModel
contient un tableau de StreamItem
s. StreamModel
possède également des propriétés d'identifiant, de nom et de description. Encore une fois, tout StreamModel
doit être codable pour le stockage et la distribution.Créer une structure d'opérateur
Comme nous l'avons mentionné précédemment, les opérateurs delay
peuvent modifier le temps de transmission StreamItem
.enum Operator {
case delay(seconds: Double)
}
Nous considérons l'opérateur delay
comme une énumération ( enum
) avec une valeur associée afin de stocker le temps de retard.Bien sûr, l'énumération Operator
doit également correspondre Codable
, ce qui inclut le codage et le décodage des valeurs associées. Voir l'implémentation complète ci-dessous.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)
}
}
}
Nous avons maintenant une bonne structure pour représenter ce flux séquentiel, qui génère des valeurs de 1 à 4 avec un deuxième intervalle de retard.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)
Convertir StreamModel en Publisher
Nous avons maintenant créé une instance du flux; cependant, si nous ne le convertissons pas en éditeur, tout n'aura aucun sens. Essayons.Tout d'abord, chaque modèle d'opérateur fait référence à l'opérateur de combinaison réel, qui doit être ajouté à cet éditeur et renvoyé à l'éditeur exploité.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()
}
}
}
À l'heure actuelle, il n'y a qu'un seul type d'opérateur - delay
. Nous en ajouterons plus au fur et à mesure.Nous pouvons maintenant commencer à utiliser des éditeurs pour tout le monde 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
}
}
Nous commençons par la valeur Just
, la généralisons à l'aide de la méthode eraseToAnyPublisher
, puis utilisons les éditeurs de tous les opérateurs associés.Au niveau, StreamModel
nous obtenons l'éditeur de l'ensemble du flux.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()
}
}
Vous l'avez deviné: nous utilisons la méthode append
pour combiner les éditeurs.Visualisation, édition et visualisation à nouveau d'un flux
Maintenant, nous pouvons simplement décoder l'éditeur, transférer et créer un StreamView (voir comment nous l'avons fait dans les articles précédents ). Et enfin et surtout: nous pouvons maintenant simplement modifier StreamModel
, ajouter des StreamItem
valeurs supplémentaires avec de nouvelles valeurs et même partager ce modèle avec d'autres appareils via Internet.Voir la démo ci-dessous. Nous pouvons maintenant apporter des modifications au flux sans changer le code.
Chapitre suivant: Sérialisation / désérialisation des filtres et des opérateurs de carte
Dans la partie suivante, nous allons ajouter plus d'opérateurs à l'énumération d'opérateur et commencer à les appliquer au niveau du thread.Jusqu'à la prochaine fois, vous pouvez trouver le code source ici dans ce référentiel combine-magic-swifui dans le dossier combine- Playground .Nous attendons vos commentaires et sommes invités à un webinaire ouvert sur le thème "Application iOS sur SwiftUI utilisant Kotlin Mobile Multiplatform".