Sauvegarde de la logique métier dans Swift Combine. Partie 1

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 filterque map, dropet scan. De plus, nous avons présenté quelques opérateurs qui connectent ( Zipet CombineLatest) ou unifient ( Mergeet 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 StreamViewet StreamViewsigne 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 JSONEncoderet 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.

  1. Un flux n'est pas la plus petite unité d'une structure. Le plus petit est la valeur du flux.
  2. 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]
}

StreamIteminclut 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 StreamItemconformer 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>]
}

StreamModelcontient un tableau de StreamItems. StreamModelpossède également des propriétés d'identifiant, de nom et de description. Encore une fois, tout StreamModeldoit ê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 delaypeuvent modifier le temps de transmission StreamItem.

enum Operator {
 case delay(seconds: Double)
}

Nous considérons l'opérateur delaycomme une énumération ( enum) avec une valeur associée afin de stocker le temps de retard.

Bien sûr, l'énumération Operatordoit é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.

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)

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, StreamModelnous 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 appendpour 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 StreamItemvaleurs 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".

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


All Articles