Se préparer pour combiner



Il y a un an et demi, j'ai chanté les louanges de RxSwift . Il m'a fallu un certain temps pour le comprendre, mais quand cela s'est produit, il n'y avait pas de retour en arrière. Maintenant, j'avais le meilleur marteau du monde, et sacrément si tout autour de moi ne ressemblait pas à un clou.

Apple a présenté le cadre Combine lors de la conférence d'été de la WWDC . À première vue, cela ressemble à une version légèrement meilleure de RxSwift. Avant de pouvoir expliquer ce que j'aime à ce sujet et ce qui ne l'est pas, nous devons comprendre quel problème Combine est conçu pour résoudre.

Programmation réactive? Et alors?


La communauté ReactiveX - dont la communauté RxSwift fait partie - explique son essence comme suit:

API pour la programmation asynchrone avec des threads observables.

Et aussi:

ReactiveX est une combinaison des meilleures idées des modèles de conception Observer et Iterator, ainsi que de la programmation fonctionnelle.

Bon ... d'accord.

Et qu'est-ce que cela signifie vraiment ?

Les bases


Pour vraiment comprendre l'essence de la programmation réactive, je trouve utile de comprendre comment nous y sommes arrivés. Dans cet article, je vais décrire comment vous pouvez regarder les types existants dans n'importe quel langage POO moderne, les tordre, puis arriver à une programmation réactive.

Dans cet article, nous allons nous plonger rapidement dans la jungle, ce qui n'est pas absolument nécessaire pour comprendre la programmation réactive.

Cependant, je considère cela comme un exercice académique curieux, en particulier en ce qui concerne la façon dont les langages fortement typés peuvent nous conduire à de nouvelles découvertes.

Alors attendez mes prochains articles si vous êtes intéressé par de nouveaux détails.

Enumerable


La « programmation réactive » que je connais est issue du langage dans lequel j'ai écrit - C #. La prémisse elle-même est assez simple:

Et si, au lieu d'extraire des valeurs d'énumérables, ils vous enverraient les valeurs elles-mêmes?

Cette idée, «pousser au lieu de tirer», a été mieux décrite par Brian Beckman et Eric Meyer. Les 36 premières minutes ... Je n'ai rien compris, mais à partir de la 36e minute, ça devient vraiment intéressant.

En bref, reformulons l'idée d'un groupe linéaire d'objets dans Swift, ainsi que d'un objet qui peut parcourir ce groupe linéaire. Vous pouvez le faire en définissant ces faux protocoles Swift:

//   ;     //    Array. protocol Enumerable { associatedtype Enum: Enumerator associatedtype Element where Self.Element == Self.Enum.Element func getEnumerator() -> Self.Enum } // ,       . protocol Enumerator: Disposable { associatedtype Element func moveNext() throws -> Bool var current: Element { get } } //          // Enumerator,         .   . protocol Disposable { func dispose() } 

Doubles


Tournons le tout et faisons des doubles . Nous enverrons les données d'où ils viennent. Et récupérez les données d'où ils sont partis. Cela semble absurde, mais supportez-le un peu.

Double énumérable


Commençons par Enumerable:

 //    ,  . protocol Enumerable { associatedtype Element where Self.Element == Self.Enum.Element associatedtype Enum: Enumerator func getEnumerator() -> Self.Enum } protocol DualOfEnumerable { //  Enumerator : // getEnumerator() -> Self.Enum //    : // getEnumerator(Void) -> Enumerator // //  , : // : Void; : Enumerator // getEnumerator(Void) → Enumerator // //     Void   Enumerator. //   -      Enumerator,   Void. // :  Enumerator; : Void func subscribe(DualOfEnumerator) } 

Depuis que getEnumerator() pris Void et a donné Enumerator , maintenant nous acceptons [double] Enumerator et donnons Void .

Je sais que c'est étrange. Ne pars pas.

Double énumérateur


Et puis qu'est-ce que DualOfEnumerator ?

 //    ,  . protocol Enumerator: Disposable { associatedtype Element // : Void; : Bool, Error func moveNext() throws -> Bool // : Void; : Element var current: Element { get } } protocol DualOfEnumerator { // : Bool, Error; : Void //   Error    func enumeratorIsDone(Bool) // : Element, : Void var nextElement: Element { set } } 

Il y a plusieurs problèmes ici:

  • Il n'y a pas de concept de propriété en ensemble uniquement dans Swift.
  • Que s'est-il passé avec les throws dans Enumerator.moveNext() ?
  • Qu'arrive-t-il au Disposable ?

Pour résoudre le problème avec la propriété set-only, nous pouvons la traiter comme ce qu'elle est vraiment - une fonction. DualOfEnumerator notre DualOfEnumerator :

 protocol DualOfEnumerator { // : Bool; : Void, Error //   Error    func enumeratorIsDone(Bool) // : Element, : Void func next(Element) } 

Pour résoudre le problème des throws , séparons l'erreur qui peut se produire dans moveNext() et travaillons avec elle comme une fonction error() distincte:

 protocol DualOfEnumerator { // : Bool, Error; : Void func enumeratorIsDone(Bool) func error(Error) // : Element, : Void func next(Element) } 

On peut faire autre chose: regardez la signature de la fin de l'itération:

 func enumeratorIsDone(Bool) 

Probablement quelque chose de similaire se produira avec le temps:

 enumeratorIsDone(false) enumeratorIsDone(false) //     enumeratorIsDone(true) 

Maintenant, simplifions les choses et n'appelons enumeratorIsDone que lorsque ... tout est vraiment prêt. Guidés par cette idée, nous simplifions le code:

 protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) } 

Prenez soin de nous


Qu'en est-il Disposable ? Que faire avec ça? Étant donné que Disposable fait partie du type Enumerator , lorsque nous obtenons le double Enumerator , il ne devrait probablement pas être du tout dans Enumerator . Au lieu de cela, il doit faire partie de DualOfEnumerable . Mais où exactement?

DualOfEnumerator ici:

 func subscribe(DualOfEnumerator) 

Si nous acceptons DualOfEnumerator , alors le Disposable ne devrait-il pas être retourné ?

Voici quel genre de double vous vous retrouvez:

 protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) } 

Appelez ça une rose, mais pas


Alors, encore une fois, voici ce que nous avons obtenu:

 protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) } 

Jouons un peu avec les noms maintenant.

Commençons par DualOfEnumerator . Nous trouverons de meilleurs noms pour les fonctions afin de décrire plus précisément ce qui se passe:

 protocol DualOfEnumerator { func onComplete() func onError(Error) func onNext(Element) } 

Tellement mieux et plus compréhensible.

Et les noms de type? Ils sont tout simplement horribles. Changeons-les un peu.

  • DualOfEnumerator - quelque chose qui suit ce qui arrive à un groupe linéaire d'objets. On peut dire qu'il observe un groupe linéaire.
  • DualOfEnumerable est un sujet d'observation. Ce que nous regardons. Par conséquent, il peut être appelé observable .

Maintenant, apportez les dernières modifications et obtenez les éléments suivants:

 protocol Observable { func subscribe(Observer)Disposable } protocol Observer { func onComplete() func onError(Error) func onNext(Element) } 

Wow


Nous venons de créer deux objets fondamentaux dans RxSwift. Vous pouvez voir leurs versions réelles ici et ici . Notez que dans le cas d'Observer, les trois fonctions on() sont combinées en une seule on(Event) , où Event est une énumération qui détermine ce qu'est l'événement - achèvement, valeur suivante ou erreur.

Ces deux types sous-tendent RxSwift et la programmation réactive.

À propos des faux protocoles


Les deux «faux» protocoles que j'ai mentionnés ci-dessus ne sont en fait pas du tout faux. Ce sont des analogues des types existants dans Swift:


Et alors?


Alors de quoi s'inquiéter?

Tant dans le développement moderne - en particulier le développement d'applications - est associé à l'asynchronie. L'utilisateur a soudainement cliqué sur un bouton. L'utilisateur a soudainement sélectionné un onglet dans UISegmentControl. L'utilisateur a soudainement sélectionné un onglet dans l'UITabBar. La prise Web nous a soudain donné de nouvelles informations. Ce téléchargement s'est soudainement - et finalement - terminé. Cette tâche d'arrière-plan s'est terminée brusquement. Cette liste s'allonge encore et encore.

Dans le monde moderne de CocoaTouch, il existe de nombreuses façons de gérer de tels événements:

  • notifications
  • rappels
  • Observation clé-valeur (KVO),
  • mécanisme cible / action.

Imaginez si tout cela pouvait se refléter dans une seule interface. Qui pourrait fonctionner avec tout type de données ou d'événements asynchrones dans l'ensemble de l'application.

Imaginez maintenant s'il y aurait tout un ensemble de fonctions qui vous permettrait de modifier ces flux , de les convertir d'un type à un autre, d'extraire des informations d'Elements ou même de les combiner avec d'autres flux.

Soudain, entre nos mains est un nouvel ensemble universel d'outils.
Et donc, nous sommes revenus au début:

API pour la programmation asynchrone avec des threads observables.

C'est ce qui fait de RxSwift un outil si puissant. Comme Combiner.

Et ensuite?


Si vous voulez en savoir plus sur RxSwift dans la pratique , alors je recommande mes cinq articles écrits en 2016 . Ils décrivent la création d'une simple application CocoaTouch, suivie d'une conversion progressive vers RxSwift.

Dans l'un des articles suivants, j'expliquerai pourquoi de nombreuses techniques décrites dans ma série d'articles pour les débutants ne sont pas applicables dans Combine, et je compare également Combine avec RxSwift.

Combiner: quel est l'intérêt?


La discussion sur Combine comprend également une discussion sur les principales différences entre celui-ci et RxSwift. Pour moi, il y en a trois:

  • la possibilité d'utiliser des classes non réactives,
  • gestion des erreurs
  • contre-pression.

Je consacrerai un article séparé à chaque article. Je vais commencer par le premier.

Caractéristiques de RxCocoa


Dans un post précédent, j'ai dit que RxSwift est plus que ... RxSwift. Il offre de nombreuses possibilités d'utilisation des contrôles d'UIKit dans le sous-projet de type mais pas tout à fait de RxCocoa. En outre, RxSwiftCommunity est allé plus loin et a implémenté de nombreuses liaisons pour des rues encore plus isolées d'UIKit, ainsi que d'autres classes CocoaTouch que RxSwift et RxCocoa ne couvrent pas encore.

Par conséquent, il est très facile d'obtenir un flux Observable cliquant, par exemple, sur UIButton. Je vais redonner cet exemple:

 let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag) 

Poids léger.

Parlons (enfin) encore de Combiner


Combine est très similaire à RxSwift. Comme le dit la documentation:

Le framework Combine fournit une API Swift déclarative pour gérer les valeurs dans le temps.

Cela semble familier: rappelez-vous la description de ReactiveX (le projet parent de RxSwift):

API pour la programmation asynchrone avec des threads observables.

Dans les deux cas, la même chose est dite. C'est juste que des termes spécifiques sont utilisés dans la description de ReactiveX. Il peut être reformulé comme suit:

Une API pour la programmation asynchrone avec des valeurs dans le temps.

Presque la même chose que pour moi.

Comme avant


Lorsque j'ai commencé à analyser l'API, il est immédiatement devenu évident que la plupart des types que je connais de RxSwift ont des options similaires dans Combine:

  • Observable → Éditeur
  • Observateur → Abonné
  • Jetable → Annulable . C'est un triomphe du marketing. Vous ne pouvez pas imaginer le nombre de regards surpris que j'ai reçus de développeurs plus impartiaux lorsque j'ai commencé à décrire Disposable dans RxSwift.
  • SchedulerType → Scheduler

Jusqu'ici tout va bien. Encore une fois, j'aime bien Annulable bien plus que Jetable. Un excellent remplacement, non seulement en termes de marketing, mais aussi en termes de description précise de l'essence de l'objet.

Plus c'est encore mieux!


Ce n'est pas immédiatement clair, mais spirituellement ils servent un seul but, et aucun d'eux ne peut donner lieu à des erreurs.


"Pause pour merde"


Tout change dès que vous commencez à plonger dans RxCocoa. Rappelez-vous l'exemple ci-dessus, dans lequel nous voulions obtenir un flux observable qui représente les clics sur UIButton? Le voici:

 let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag) 

Combiner nécessite ... beaucoup plus de travail pour faire de même.

Combine ne fournit aucune capacité de liaison aux objets UIKit.

C'est ... juste une déception irréelle.

Voici un moyen courant d'obtenir UIControl.Event à partir d' UIControl à l' aide de Combine:

 class ControlPublisher<T: UIControl>: Publisher { typealias ControlEvent = (control: UIControl, event: UIControl.Event) typealias Output = ControlEvent typealias Failure = Never let subject = PassthroughSubject<Output, Failure>() convenience init(control: UIControl, event: UIControl.Event) { self.init(control: control, events: [event]) } init(control: UIControl, events: [UIControl.Event]) { for event in events { control.addTarget(self, action: #selector(controlAction), for: event) } } @objc private func controlAction(sender: UIControl, forEvent event: UIControl.Event) { subject.send(ControlEvent(control: sender, event: event)) } func receive<S>(subscriber: S) where S : Subscriber, ControlPublisher.Failure == S.Failure, ControlPublisher.Output == S.Input { subject.receive(subscriber: subscriber) } } 

Ici ... beaucoup plus de travail. Au moins, l'appel ressemble à:

 ControlPublisher(control: self.button, event: .touchUpInside) .sink { print("Tap!") } 

A titre de comparaison, RxCocoa fournit un cacao agréable et savoureux sous forme de liaisons aux objets UIKit:

 self.button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) 

En eux-mêmes, ces défis sont finalement très similaires. La seule chose frustrante est que j'ai dû écrire ControlPublisher moi-même pour arriver à ce point. De plus, RxSwift et RxCocoa sont très bien testés et sont utilisés dans des projets bien plus que les miens.

À titre de comparaison, mon ControlPublisher est apparu seulement ... maintenant. Ce n'est qu'en raison du nombre de clients (zéro) et du temps d'utilisation dans le monde réel (presque zéro par rapport à RxCocoa) que mon code peut être considéré comme infiniment plus dangereux.

Bummer.

Aide communautaire?


Honnêtement, rien n'empêche la communauté de créer son propre «CombineCocoa» open source, qui comblerait le vide de RxCocoa tout comme le faisait RxSwiftCommunity.

Cependant, je considère cela comme un énorme inconvénient de Combine. Je ne veux pas réécrire l'intégralité de RxCocoa, seulement pour obtenir des liaisons aux objets UIKit.

Si je décide de parier sur SwiftUI , je suppose que cela éliminera le problème du manque de fixations. Même ma petite application contient un tas de code d'interface utilisateur. Jeter tout cela juste pour sauter dans le train combiné serait au moins stupide, voire dangereux.

Soit dit en passant, l'article de la documentation Réception et gestion d'événements avec Combine décrit brièvement comment recevoir et traiter des événements dans Combine. L'introduction est bonne, elle montre comment extraire une valeur d'un champ de texte et l'enregistrer dans un objet de modèle personnalisé. La documentation montre également l'utilisation d'opérateurs pour effectuer des modifications plus avancées sur le flux en question.

Exemple


Passons à la fin de la documentation, où l'exemple de code est:

 let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel) 

J'ai ... beaucoup de problèmes avec ça.

Vous avertir que je n'aime pas ça


Les deux premières lignes me posent le plus de questions:

 let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) 

NotificationCenter est quelque chose comme un bus d'application (ou même un bus système) dans lequel beaucoup peuvent jeter des données ou attraper des informations en passant. Cette solution est de la catégorie tout-pour-tout, comme le souhaitent les créateurs. Et il y a vraiment de nombreuses situations où vous devrez peut-être savoir, disons, que le clavier vient d'être affiché ou caché. NotificationCenter est un excellent moyen de diffuser ce message dans tout le système.

Mais pour moi, NotificationCenter est un code avec un étranglement . Il y a des moments (comme recevoir une notification sur le clavier) lorsque NotificationCenter est en fait la meilleure solution possible au problème. Mais trop souvent pour moi, NotificationCenter est la solution la plus pratique . Il est vraiment très pratique de déposer quelque chose dans NotificationCenter et de le récupérer ailleurs dans l'application.

De plus, le NotificationCenter est de type "chaîne" , c'est-à-dire que vous pouvez facilement faire l'erreur de la notification que vous essayez de publier ou d'écouter. Swift fait tout son possible pour améliorer la situation, mais le même NSString se trouve toujours sous le capot.

À propos de KVO


Sur la plate-forme Apple, il existe depuis longtemps un moyen populaire de recevoir des notifications de modifications dans différentes parties du code: l'observation des valeurs-clés (KVO). Apple le décrit comme ceci:

Il s'agit d'un mécanisme qui permet aux objets de recevoir des notifications de modifications des propriétés spécifiées d'autres objets.

Grâce à un tweet de Gui Rambo, j'ai remarqué qu'Apple avait ajouté des fixations KVO à Combine. Cela pourrait signifier que je pourrais me débarrasser des nombreuses déceptions concernant le manque d'analogue RxCocoa dans Combine. Si je peux utiliser KVO, cela éliminera probablement le besoin de CombineCocoa, pour ainsi dire.

J'ai essayé de trouver un exemple d'utilisation de KVO pour obtenir une valeur à partir d'un UITextField et la sortir sur la console:

 let sub = self.textField.publisher(for: \UITextField.text) .sink(receiveCompletion: { _ in print("Completed") }, receiveValue: { print("Text field is currently \"\($0)\"") }) 

Ça a l'air bien, passez à autre chose?

Pas si vite, mes amis.

J'ai oublié la vérité inconfortable :

UIKit, dans l'ensemble, n'est pas compatible avec KVO.

Et sans le support de KVO, mon idée ne fonctionnera pas. Mes vérifications l'ont confirmé: le code ne sort rien sur la console lorsque j'entre du texte dans le champ.

Donc, mes espoirs de me débarrasser du besoin de fixations UIKit étaient beaux, mais pas pour longtemps.

Le nettoyage


Un autre problème de Combine est qu'il n'est toujours pas clair où et comment libérer des ressources dans les objets annulables . Il semble que nous devrions les stocker dans des variables d'instance. Mais je ne me souviens pas que dans la documentation officielle, quelque chose ait été dit au sujet du nettoyage.

RxSwift a un DisposeBag terriblement nommé mais incroyablement pratique. Il n'est pas moins facile de créer CancelBag dans Combine, mais je ne suis pas sûr que dans ce cas, c'est la meilleure solution.

Dans le prochain article, nous parlerons de la gestion des erreurs dans RxSwift et Combine, des avantages et des inconvénients des deux approches.

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


All Articles