Créez votre éditeur dans Combine


Aujourd'hui, je voudrais vous montrer comment créer votre propre éditeur dans le nouveau framework Combine d'Apple.


Et donc, pour commencer, nous devons rappeler brièvement comment les parties fondamentales de Combine interagissent les unes avec les autres, à savoir Publisher, Subscription, Subscriber.


  • L'abonné rejoint l'éditeur
  • L'éditeur envoie un abonnement d'abonnement
  • L'abonné demande N valeurs à l'abonnement
  • L'éditeur envoie N valeurs ou moins
  • L'éditeur envoie un signal d'achèvement

Éditeur


Commençons donc à créer notre éditeur. En ce qui concerne la documentation Apple, nous verrons que Publisher est un protocole.


public protocol Publisher { associatedtype Output associatedtype Failure : Error func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input } 

Où Output est le type de valeurs transmises par cet éditeur, Failure est le type d'erreur qui doit suivre le protocole Error.


Et la fonction de réception (_: Subscriber), qui sera appelée pour ajouter Subscriber à cet éditeur à l'aide de subscribe (_ :).


Par exemple, nous implémentons Publisher, qui générera pour nous des numéros de Fibonacci .


 struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never } 

Étant donné que la séquence se compose de nombres, le type de sortie sera Output de type Int et Failure sera un type spécial de Never, indiquant que ce serveur de publication n'échouera jamais.


Pour plus de flexibilité, nous spécifions la limite du nombre d'éléments que nous voulons recevoir et encapsulons cette valeur dans un objet de configuration de notre éditeur.


 struct FibonacciConfiguration { var count: UInt } 

Examinons de plus près ce code, var count: UInt ressemble à une bonne option, mais son utilisation nous limite à la plage de valeurs valides du type UInt et il n'est pas non plus entièrement clair ce qu'il faut indiquer si nous voulons toujours avoir une séquence illimitée.


Au lieu d'UInt, nous utiliserons le type Subscribers.Demand, qui est défini dans Combine, où il est également décrit comme le type envoyé de l'Abonné à l'Editeur via l'Abonnement. En termes simples, il montre la nécessité d'éléments, combien d'éléments sont demandés par l'abonné. illimité - pas limité, aucun - pas du tout, max (N) - pas plus de N fois.


  public struct Demand : Equatable, Comparable, Hashable, Codable, CustomStringConvertible { public static let unlimited: Subscribers.Demand public static let none: Subscribers.Demand ///  Demand.max(0) @inlinable public static func max(_ value: Int) -> Subscribers.Demand .... } 

Nous réécrivons FibonacciConfiguration en changeant le type en un nouveau pour le compte.


 struct FibonacciConfiguration { var count: Subscribers.Demand } 

Revenons à Publisher et implémentons la méthode de réception (_: Subscriber), comme nous nous en souvenons, cette méthode est nécessaire pour ajouter Subscriber à Publisher. Et il le fait avec un abonnement, l'éditeur doit créer un abonnement et transférer l'abonnement à l'abonné.


  func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } 

Il s'agit d'une fonction générique qui prend l'abonné comme paramètre, et les valeurs de sortie de Publisher doivent correspondre aux valeurs d'entrée de l'abonné (Output == S.Input), de même pour les erreurs. Cela est nécessaire pour "connecter" Publisher'a et Subscriber'a.


Dans la fonction elle-même, créez un abonnement FibonacciSubscription, dans le constructeur nous transférons l'abonné et la configuration. Après cela, l'abonnement est transféré à l'abonné.


Notre éditeur est prêt, au final nous avons:


 struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } 

Comme vous pouvez le voir, Publisher lui-même ne contient aucune logique pour générer une séquence Fibonacci; toute la logique sera dans la classe d'abonnement - FibonacciSubscription.


Comme vous pouvez déjà le deviner, la classe FibonacciSubscription suivra le protocole Subscription, regardons la définition de ce protocole.


 public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible { func request(_ demand: Subscribers.Demand) } 

La fonction request (_: Subscribers.Demand) indique à Publisher qu'il peut envoyer plus de valeurs à l'abonné. C'est dans cette méthode que sera la logique d'envoi des numéros de Fibonacci.
Nous devons également implémenter le protocole Cancellable et implémenter la fonction cancel ().


 public protocol Cancellable { func cancel() } 

Et suivez simplement le protocole CustomCombineIdentifierConvertible et définissez la variable en lecture seule combineIdentifier.


 public protocol CustomCombineIdentifierConvertible { var combineIdentifier: CombineIdentifier { get } } 

Il y a une clarification ici, si vous faites défiler juste en dessous de la définition du protocole CustomCombineIdentifierConvertible dans Combine, vous pouvez voir que Combine fournit une extension pour ce protocole, qui a la forme -


 extension CustomCombineIdentifierConvertible where Self : AnyObject { public var combineIdentifier: CombineIdentifier { get } } 

Ce qui nous indique que la définition de la variable combineIdentifier: CombineIdentifier est fournie par défaut si le type qui suit ce protocole suit également le protocole AnyObject, à savoir si ce type est une classe. FibonacciSubscription est une classe, nous obtenons donc la définition de variable par défaut.


Abonnement


Et nous allons donc commencer à mettre en œuvre notre abonnement Fibonacci.


 private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } ... } 

Comme vous pouvez le voir, FibonacciConfiguration contient un lien fort vers l'Abonné, en d'autres termes, est le propriétaire de l'Abonné. C'est un point important, l'abonnement est responsable de la rétention de l'abonné, et il doit le maintenir jusqu'à ce qu'il termine son travail, se termine avec une erreur ou avec annulé.


Ensuite, nous implémentons la méthode cancel () du protocole Cancellable.


 func cancel() { subscriber = nil } 

Si vous définissez l'abonné sur zéro, il est inaccessible à un abonnement.


Nous sommes maintenant prêts à commencer la mise en œuvre de l'envoi de numéros Fibonacci.
nous implémentons la méthode de demande (_: Subscribers.Demand).


 func request(_ demand: Subscribers.Demand) { // 1 guard count > .none else { subscriber?.receive(completion: .finished) return } // 2 count -= .max(1) subscriber?.receive(0) if count == .none { subscriber?.receive(completion: .finished) return } // 3 count -= .max(1) subscriber?.receive(1) if count == .none { subscriber?.receive(completion: .finished) return } // 4 var prev = 0 var current = 1 var temp: Int while true { temp = prev prev = current current += temp subscriber?.receive(current) count -= .max(1) if count == .none { subscriber?.receive(completion: .finished) return } } } 

1) Dès le début, nous vérifions le nombre d'éléments que l'éditeur peut nous fournir, voire pas du tout, puis terminons l'envoi et envoyons à l'Abonné un signal indiquant que l'envoi des numéros est terminé.
2) S'il y a un besoin, réduisez le nombre total de numéros demandés de un, envoyez le premier élément de la séquence de Fibonacci à l'Abonné, à savoir 0, puis vérifiez à nouveau combien d'éléments supplémentaires Publisher peut nous donner, sinon, envoyez un signal à l'Abonné pour terminer .
3) La même approche qu'au paragraphe 2), mais uniquement pour le deuxième élément de la séquence de Fibonacci.
4) Si plus de 2 éléments sont requis, nous implémentons un algorithme itératif pour trouver les numéros de Fibonacci, où à chaque étape nous transférerons le numéro suivant de la séquence de Fibonacci vers Subscriber'y et vérifierons également combien d'éléments Publisher peut encore fournir. Si Publisher ne fournit plus de nouveaux numéros, envoyez à l'Abonné un signal d'achèvement.


Pour le moment, nous avons écrit un tel code
 struct FibonacciConfiguration { var count: Subscribers.Demand } struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) { // 1 guard count > .none else { subscriber?.receive(completion: .finished) return } // 2 count -= .max(1) subscriber?.receive(0) if count == .none { subscriber?.receive(completion: .finished) return } // 3 count -= .max(1) subscriber?.receive(1) if count == .none { subscriber?.receive(completion: .finished) return } // 4 var prev = 0 var current = 1 var temp: Int while true { temp = prev prev = current current += temp subscriber?.receive(current) count -= .max(1) if count == .none { subscriber?.receive(completion: .finished) return } } } } 

Premier test


Maintenant, nous allons tester ce que nous avons, nous avons notre éditeur et abonnement, nous n'avons pas assez de Sibscriber, Combine fournit 2 Sibscriber de la boîte: couler et assigner.


  • sink - cette méthode crée un abonné et demande immédiatement un nombre illimité de valeurs.
  • assign - définit chaque élément de Publisher sur une propriété d'objet.

évier est bien adapté à notre objectif, une attention particulière doit être accordée au fait qu'il demande un nombre illimité de valeurs.


Et ici, nous devons faire une distinction importante, notre éditeur dans la variable count détermine le nombre d'éléments que notre éditeur peut donner et ces conditions sont déterminées par nous-mêmes. En principe, nous pourrions nous passer de cette variable et ne pas être limités dans la transmission des nombres de Fibonacci, mais très vite nous dépasserions la plage des valeurs admissibles de type Int.
Le cas avec récepteur est différent, chaque abonné détermine le nombre de valeurs qu'il souhaite recevoir, récepteur demande un nombre illimité de valeurs, ce qui signifie qu'il recevra des valeurs jusqu'à ce qu'il reçoive un signal d'achèvement, d'erreur ou d'annulation.


Pour la commodité de l'utilisation de notre éditeur, nous ajoutons sa création à l'extension de protocole des éditeurs.


 extension Publishers { private static func fibonacci(configuration: FibonacciConfiguration) -> FibonacciPublisher { FibonacciPublisher(configuration: configuration) } static func fibonacci(count: Subscribers.Demand = .max(6)) -> FibonacciPublisher { FibonacciPublisher(configuration: FibonacciConfiguration(count: count)) } } 

Et donc essayez notre éditeur


 Publishers.fibonacci(count: .max(10)) .sink { value in print(value, terminator: " ") } // print 0 1 1 2 3 5 8 13 21 34 - OK 

Et maintenant, les cas limites


 Publishers.fibonacci(count: .max(1)) .sink { value in print(value, terminator: " ") } // prinst 0 - OK Publishers.fibonacci(count: .max(2)) .sink { value in print(value, terminator: " ") } // prints 0 1 - OK Publishers.fibonacci(count: .none) .print() //    publisher'a .sink { value in print(value, terminator: " ") } // prints receive finished - OK 

Mais que se passe-t-il si vous spécifiez .unlimited?


 Publishers.fibonacci(count: .unlimited) .print() .sink { value in print(value, terminator: " ") } // prints 0 1 1 2 3 5 8 13 21 ...   ,     Int. 

Comment pouvez-vous utiliser .unlimited, mais être capable de produire plusieurs numéros? Pour ce faire, nous avons besoin de l'opérateur .prefix (_), qui fonctionne de la même manière que .prefix (_) des collections, à savoir, il ne laisse que les N premiers éléments.


 Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in } // prints 0 1 1 2 3 cancel   ,       Int. 

Quel est le problème? Peut-être en .prefix (_)? Faisons une petite expérience sur la séquence standard de Foundation.


 //   1 2 3 4 5 6 7 8 ... 1... .publisher .print() .prefix(5) .sink { _ in } // prints 1 2 3 4 5 cancel -  

Comme nous pouvons le voir, le code ci-dessus a fonctionné correctement, alors le problème est dans notre implémentation de Publisher.
Nous regardons les journaux de .print () et voyons qu'après N requêtes, à partir de .prefix (_), nous appelons cancel () sur notre FibonacciSubscription, où nous définissons abonné à nil.


  func cancel() { subscriber = nil } 

Si vous ouvrez la pile d'appels, vous pouvez voir que cancel () est appelé à partir de la requête (_ :), à savoir lors de l'appel à l'abonné? .Receive (_). D'où nous pouvons conclure qu'à un certain moment dans la demande (_ :) l'abonné peut devenir nul et ensuite nous devons arrêter le travail de génération de nouveaux numéros. Ajoutez cette condition à notre code.


  func request(_ demand: Subscribers.Demand) { // 1 guard count > .none else { subscriber?.receive(completion: .finished) return } // 2 count -= .max(1) subscriber?.receive(0) guard let _ = subscriber else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } // 3 count -= .max(1) subscriber?.receive(1) guard let _ = subscriber else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } // 4 var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber { // new temp = prev prev = current current += temp subscriber.receive(current) count -= .max(1) if count == .none { subscriber.receive(completion: .finished) return } } } 

Exécutez maintenant notre code de test.


 Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in } // prints 0 1 1 2 3 cancel -  

Vous avez le comportement attendu.


Abonné


Et notre abonnement Fibonacci est-il prêt? Pas vraiment, dans nos tests, nous n'avons utilisé qu'un abonné récepteur qui demande un nombre illimité de numéros, mais que se passe-t-il si nous utilisons un abonné à la place qui attendra un certain nombre limité de numéros. Combine ne fournit pas un tel abonné, mais qu'est-ce qui nous empêche d'écrire le nôtre? Vous trouverez ci-dessous la mise en œuvre de notre abonné Fibonacci.


 class FibonacciSubscriber: Subscriber { typealias Input = Int typealias Failure = Never var limit: Subscribers.Demand init(limit: Subscribers.Demand) { self.limit = limit } func receive(subscription: Subscription) { subscription.request(limit) } func receive(_ input: Input) -> Subscribers.Demand { .none } func receive(completion: Subscribers.Completion<Failure>) { print("Subscriber's completion: \(completion)") } } 

Et donc notre abonné Fibonacci a une propriété de limite, qui détermine le nombre d'éléments que cet abonné veut recevoir. Et cela se fait dans la méthode receive (_: Subscription), où nous indiquons à l'abonnement le nombre d'éléments dont nous avons besoin. Il est également nécessaire de noter la fonction receive (_: Input) -> Subscribers.Demand, cette fonction est appelée lorsqu'une nouvelle valeur est reçue, comme valeur de retour nous indiquons combien d'éléments supplémentaires nous voulons recevoir: .aucun - pas du tout, .max (N) N pièces , au total, le nombre total d'éléments reçus sera égal à la somme de la valeur de l'abonnement envoyé en réception (_: abonnement) et de toutes les valeurs de retour de réception (_: entrée) -> Subscribers.Demand.


Deuxième test


Essayons d'utiliser FibonacciSubscriber.


 let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber) // prints 0 1 1 2 3 -     0 1 1 

Comme nous le voyons, notre éditeur a envoyé 5 valeurs, au lieu de 3. Pourquoi? Parce que la méthode request (_: Subscribers.Demand) de FibonacciSubscription'a ne prend pas en compte le besoin de l'abonné, corrigeons-le, pour cela nous ajoutons une propriété supplémentaire demandée, à travers laquelle nous suivrons le besoin de l'abonné.


 private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand var requested: Subscribers.Demand = .none // new init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) { guard count > .none else { subscriber?.receive(completion: .finished) return } requested += demand // new count -= .max(1) requested -= .max(1) // new requested += subscriber?.receive(0) ?? .none // new guard let _ = subscriber, requested > .none else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } count -= .max(1) requested -= .max(1) // new requested += subscriber?.receive(1) ?? .none // new guard let _ = subscriber, requested > .none else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber, requested > .none { // new temp = prev prev = current current += temp requested += subscriber.receive(current) // new count -= .max(1) requested -= .max(1) // new if count == .none { subscriber.receive(completion: .finished) return } } } } 

Troisième essai


 let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber) // prints 0 1 1 - OK 

L'éditeur fonctionne désormais correctement.


Code final
 import Foundation import Combine struct FibonacciConfiguration { var count: Subscribers.Demand } struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand var requested: Subscribers.Demand = .none init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) { guard count > .none else { subscriber?.receive(completion: .finished) return } requested += demand count -= .max(1) requested -= .max(1) requested += subscriber?.receive(0) ?? .none guard let _ = subscriber, requested > .none else { return } if count == .none { subscriber?.receive(completion: .finished) return } count -= .max(1) requested -= .max(1) requested += subscriber?.receive(1) ?? .none guard let _ = subscriber, requested > .none else { return } if count == .none { subscriber?.receive(completion: .finished) return } var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber, requested > .none { temp = prev prev = current current += temp requested += subscriber.receive(current) count -= .max(1) requested -= .max(1) if count == .none { subscriber.receive(completion: .finished) return } } } } extension Publishers { private static func fibonacci(configuration: FibonacciConfiguration) -> FibonacciPublisher { FibonacciPublisher(configuration: configuration) } static func fibonacci(count: Subscribers.Demand = .max(6)) -> FibonacciPublisher { FibonacciPublisher(configuration: FibonacciConfiguration(count: count)) } } class FibonacciSubscriber: Subscriber { typealias Input = Int typealias Failure = Never var limit: Subscribers.Demand init(limit: Subscribers.Demand) { self.limit = limit } func receive(subscription: Subscription) { subscription.request(limit) } func receive(_ input: Input) -> Subscribers.Demand { .none } func receive(completion: Subscribers.Completion<Failure>) { print("Subscriber's completion: \(completion)") } } Publishers.fibonacci(count: .max(4)) .print() .sink { _ in } let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber) 

Résultat


J'espère que cet article vous a permis de mieux comprendre ce que sont Publisher, Subscription et Subscriber, comment ils interagissent les uns avec les autres, et sur quels points vous devez faire attention lorsque vous décidez d'implémenter votre Publisher. Tous commentaires, clarifications à l'article sont les bienvenus.

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


All Articles