Preparándose para combinar



Hace un año y medio, canté las alabanzas de RxSwift . Me tomó un tiempo darme cuenta, pero cuando eso sucedió, no hubo vuelta atrás. Ahora tenía el mejor martillo del mundo, y maldita sea si todo a mi alrededor no parecía un clavo.

Apple presentó el marco Combine en la WWDC Summer Conference. A primera vista, parece una versión ligeramente mejor de RxSwift. Antes de que pueda explicar lo que me gusta y lo que no, necesitamos entender qué problema está diseñado para resolver Combine.

Programación reactiva? ¿Y qué?


La comunidad ReactiveX, de la cual forma parte la comunidad RxSwift, explica su esencia de la siguiente manera:

API para programación asincrónica con hilos observables.

Y también:

ReactiveX es una combinación de las mejores ideas de los patrones de diseño de Observer e Iterator, así como la programación funcional.

Bueno ... esta bien.

¿Y qué significa esto realmente ?

Los fundamentos


Para comprender realmente la esencia de la programación reactiva, me resulta útil comprender cómo llegamos a ella. En este artículo, describiré cómo puede ver los tipos existentes en cualquier lenguaje OOP moderno, torcerlos y luego pasar a la programación reactiva.

En este artículo, profundizaremos rápidamente en la jungla, lo que no es absolutamente necesario para comprender la programación reactiva.

Sin embargo, considero que este es un ejercicio académico curioso, especialmente en términos de cuán fuertemente mecanografiados los idiomas pueden llevarnos a nuevos descubrimientos.

Así que espera mis próximas publicaciones si estás interesado en nuevos detalles.

Enumerable


La " programación reactiva " conocida por mí se originó en el lenguaje en el que una vez escribí: C #. La premisa en sí es bastante simple:

¿Qué pasa si, en lugar de extraer valores de enumerables, le enviarán los valores ellos mismos?

Esta idea, "empujar en lugar de tirar", fue mejor descrita por Brian Beckman y Eric Meyer. Los primeros 36 minutos ... No entendí nada, pero a partir del minuto 36 se vuelve realmente interesante.

En resumen, reformulemos la idea de un grupo lineal de objetos en Swift, así como un objeto que pueda iterar sobre este grupo lineal. Puede hacer esto definiendo estos protocolos falsos de 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() } 

Dobles


Vamos a darle la vuelta y hacer dobles . Enviaremos datos a su origen. Y obtenga los datos de donde se fueron. Suena absurdo, pero aguanta un poco.

Doble enumerable


Comencemos con 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) } 

Como getEnumerator() tomó Void y le dio Enumerator , ahora aceptamos el [doble] Enumerator y le damos Void .

Sé que esto es extraño. No te vayas.

Doble enumerador


¿Y entonces qué es 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 } } 

Hay varios problemas aquí:

  • No existe el concepto de una propiedad de solo conjunto en Swift.
  • ¿Qué pasó con los throws en Enumerator.moveNext() ?
  • ¿Qué le sucede a Disposable ?

Para solucionar el problema con la propiedad de solo conjunto, podemos tratarlo como lo que realmente es: una función. DualOfEnumerator nuestro DualOfEnumerator :

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

Para resolver el problema con los throws , moveNext() el error que puede ocurrir en moveNext() y moveNext() con él como una función separada de error() :

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

Podemos hacer algo más: eche un vistazo a la firma de la finalización de la iteración:

 func enumeratorIsDone(Bool) 

Probablemente algo similar sucederá con el tiempo:

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

Ahora, simplifiquemos las cosas y llamemos a enumeratorIsDone solo cuando ... todo esté realmente listo. Guiados por esta idea, simplificamos el código:

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

Cuidarnos


¿Qué hay de Disposable ? ¿Qué hacer con eso? Dado que Disposable es parte del tipo Enumerator , cuando obtenemos el Enumerator doble , probablemente no debería estar en el Enumerator . En cambio, debería ser parte de DualOfEnumerable . ¿Pero dónde exactamente?

DualOfEnumerator aquí DualOfEnumerator :

 func subscribe(DualOfEnumerator) 

Si aceptamos DualOfEnumerator , ¿no debería devolverse Disposable ?

Este es el tipo de doble que obtienes al final:

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

Llámalo rosa, aunque no


Entonces, una vez más, esto es lo que tenemos:

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

Juguemos un poco con los nombres ahora.

Comencemos con DualOfEnumerator . Vamos a encontrar mejores nombres para las funciones para describir con mayor precisión lo que está sucediendo:

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

Mucho mejor y más comprensible.

¿Qué pasa con los nombres de tipo? Son simplemente horribles. Vamos a cambiarlos un poco.

  • DualOfEnumerator : algo que sigue lo que le sucede a un grupo lineal de objetos. Podemos decir que observa un grupo lineal.
  • DualOfEnumerable es un tema de observación. Lo que estamos viendo Por lo tanto, se puede llamar observable .

Ahora haga los cambios finales y obtenga lo siguiente:

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

Wow


Acabamos de crear dos objetos fundamentales en RxSwift. Puedes ver sus versiones reales aquí y aquí . Tenga en cuenta que en el caso de Observer, las tres funciones on() se combinan en una on(Event) , donde Event es una enumeración que determina cuál es el evento: finalización, siguiente valor o error.

Estos dos tipos son la base de RxSwift y la programación reactiva.

Sobre protocolos falsos


Los dos protocolos "falsos" que mencioné anteriormente en realidad no son falsos. Estos son análogos de los tipos existentes en Swift:


¿Y qué?


Entonces, ¿de qué preocuparse?

Tanto en el desarrollo moderno, especialmente el desarrollo de aplicaciones, está asociado con la asincronía. El usuario de repente hizo clic en un botón. El usuario de repente seleccionó una pestaña en el UISegmentControl. El usuario de repente seleccionó una pestaña en la UITabBar. El portal web de repente nos dio nueva información. Esta descarga de repente, y finalmente, terminó. Esta tarea de fondo terminó abruptamente. Esta lista sigue y sigue.

En el mundo moderno de CocoaTouch, hay muchas maneras de manejar tales eventos:

  • notificaciones
  • devoluciones de llamada
  • Observación de valor clave (KVO),
  • objetivo / mecanismo de acción.

Imagínese si todo pudiera reflejarse en una sola interfaz. Lo que podría funcionar con cualquier tipo de datos asincrónicos o eventos dentro de toda la aplicación.

Ahora imagine si habría un conjunto completo de funciones que le permite modificar estas secuencias , convertirlas de un tipo a otro, extraer información de Elements o incluso combinarlas con otras secuencias.

De repente, en nuestras manos hay un nuevo conjunto universal de herramientas.
Y así, volvimos al principio:

API para programación asincrónica con hilos observables.

Esto es lo que hace que RxSwift sea una herramienta tan poderosa. Como combinar.

Que sigue


Si desea leer más sobre RxSwift en la práctica , le recomiendo mis cinco artículos escritos en 2016 . Describen la creación de una aplicación CocoaTouch simple, seguida de una conversión por fases a RxSwift.

En uno de los siguientes artículos explicaré por qué muchas de las técnicas descritas en mi serie de artículos para principiantes no son aplicables en Combine, y también comparo Combine con RxSwift.

Combinar: ¿cuál es el punto?


La discusión de Combine también incluye una discusión de las principales diferencias entre él y RxSwift. Para mí hay tres de ellos:

  • la posibilidad de usar clases no reactivas,
  • manejo de errores
  • contrapresión

Dedicaré un artículo separado a cada artículo. Comenzaré con el primero.

Características de RxCocoa


En una publicación anterior, dije que RxSwift es más que ... RxSwift. Ofrece numerosas posibilidades para utilizar controles de UIKit en el subproyecto de tipo pero no del todo de RxCocoa. Además, RxSwiftCommunity fue más allá e implementó muchos enlaces para calles secundarias aún más aisladas de UIKit, así como algunas otras clases de CocoaTouch que RxSwift y RxCocoa aún no cubren.

Por lo tanto, es muy fácil obtener una secuencia Observable haciendo clic, por ejemplo, en UIButton. Daré este ejemplo nuevamente:

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

Peso ligero

Hablemos (finalmente) de Combine


Combinar es muy similar a RxSwift. Como dice la documentación:

El marco Combinar proporciona una API Swift declarativa para manejar valores a lo largo del tiempo.

Suena familiar: recuerde la descripción de ReactiveX (el proyecto principal para RxSwift):

API para programación asincrónica con hilos observables.

En ambos casos, se dice lo mismo. Es solo que se usan términos específicos en la descripción de ReactiveX. Se puede reformular de la siguiente manera:

Una API para programación asincrónica con valores a lo largo del tiempo.

Casi lo mismo que para mí.

Igual que antes


Cuando comencé a analizar la API, inmediatamente se hizo evidente que la mayoría de los tipos que conozco de RxSwift tienen opciones similares en Combinar:

  • Observable → Editor
  • Observador → Suscriptor
  • Desechable → Cancelable . Este es un triunfo del marketing. No puedes imaginar cuántas miradas de sorpresa recibí de desarrolladores más imparciales cuando comencé a describir Desechable en RxSwift.
  • SchedulerType → Scheduler

Hasta ahora todo bien. Una vez más, me gusta mucho más Cancelable que Desechable. Un gran reemplazo, no solo en términos de marketing, sino también en términos de una descripción precisa de la esencia del objeto.

¡Más es aún mejor!


Esto no está claro de inmediato, pero espiritualmente sirven para un propósito, y ninguno de ellos puede dar lugar a errores.

  • Soltero → Futuro
  • SubjectType → Subject
  • PublishSubject → PassthroughSubject

"Romper para caca"


Todo cambia tan pronto como comienzas a profundizar en RxCocoa. ¿Recuerda el ejemplo anterior, en el que queríamos obtener una secuencia Observable que representa los clics en UIButton? Aquí esta:

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

Combinar requiere ... mucho más trabajo para hacer lo mismo.

Combine no proporciona ninguna capacidad para enlazar objetos UIKit.

Esto es ... solo un fastidio irreal.

Aquí hay una forma común de obtener UIControl.Event de UIControl usando 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) } } 

Aquí ... mucho más trabajo. Al menos la llamada se ve así:

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

A modo de comparación, RxCocoa proporciona un cacao agradable y sabroso en forma de enlaces a objetos UIKit:

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

Por sí mismos, estos desafíos son, en última instancia, realmente muy similares. Lo único frustrante es que tuve que escribir ControlPublisher para llegar a este punto. Además, RxSwift y RxCocoa están muy bien probados y se utilizan en proyectos mucho más que los míos.

A modo de comparación, mi ControlPublisher apareció solo ... ahora. Solo por la cantidad de clientes (cero) y el tiempo de uso en el mundo real (casi cero en comparación con RxCocoa) puede mi código considerarse infinitamente más peligroso.

Bummer

Ayuda comunitaria?


Honestamente, nada impide que la comunidad cree su propio código abierto "CombineCocoa", que llenaría la brecha de RxCocoa al igual que lo hizo RxSwiftCommunity.

Sin embargo, considero que esto es un gran inconveniente de Combine. No quiero reescribir todo el RxCocoa, solo para obtener enlaces a objetos UIKit.

Si decido apostar por SwiftUI , supongo que esto eliminará el problema de la falta de enlaces. Incluso mi pequeña aplicación contiene un montón de código de interfaz de usuario. Lanzar todo esto solo para saltar al tren Combinado sería al menos estúpido, o incluso peligroso.

Por cierto, el artículo en la documentación Recepción y manejo de eventos con Combine describe brevemente cómo recibir y procesar eventos en Combine. La introducción es buena, muestra cómo extraer un valor de un campo de texto y guardarlo en un objeto de modelo personalizado. La documentación también demuestra el uso de operadores para realizar algunas modificaciones más avanzadas a la secuencia en cuestión.

Ejemplo


Pasemos al final de la documentación, donde el ejemplo de código es:

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

Tengo ... muchos problemas con esto.

Notificarte que no me gusta


Las dos primeras líneas me causan más preguntas:

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

NotificationCenter es algo así como un bus de aplicación (o incluso un bus de sistema) en el que muchos pueden arrojar datos o atrapar fragmentos de información que vuelan. Esta solución es de la categoría de todos y para todos, según lo previsto por los creadores. Y realmente hay muchas situaciones en las que es posible que necesite averiguar, por ejemplo, que el teclado se mostró u ocultó. NotificationCenter es una excelente manera de distribuir este mensaje en todo el sistema.

Pero para mí NotificationCenter es un código con un estrangulador . Hay momentos (como recibir una notificación sobre el teclado) cuando NotificationCenter es realmente la mejor solución posible al problema. Pero con demasiada frecuencia para mí NotificationCenter es la solución más conveniente . Es realmente muy conveniente dejar caer algo en NotificationCenter y recogerlo en otro lugar de la aplicación.

Además, NotificationCenter tiene el tipo de "cadena" , es decir, puede cometer fácilmente el error de qué notificación está intentando publicar o escuchar. Swift está haciendo todo lo posible para mejorar la situación, pero sigue siendo el mismo NSString debajo del capó.

Sobre KVO


En la plataforma Apple, ha habido una forma popular de recibir notificaciones de cambios en diferentes partes del código: observación de valor clave (KVO). Apple lo describe así:

Este es un mecanismo que permite que los objetos reciban notificaciones de cambios en las propiedades especificadas de otros objetos.

Gracias a un tweet de Gui Rambo, noté que Apple agregó enlaces KVO a Combine. Esto podría significar que podría deshacerme de las muchas decepciones sobre la falta de un análogo de RxCocoa en Combine. Si puedo usar KVO, esto probablemente eliminará la necesidad de CombineCocoa, por así decirlo.

Traté de encontrar un ejemplo del uso de KVO para obtener un valor de un UITextField y UITextField a la consola:

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

Se ve bien, sigue adelante?

No tan rápido, amigos.

Olvidé la incómoda verdad :

UIKit, en general, no es compatible con KVO.

Y sin el soporte de KVO, mi idea no funcionará. Mis comprobaciones confirmaron esto: el código no muestra nada en la consola cuando ingreso texto en el campo.

Por lo tanto, mis esperanzas de deshacerme de la necesidad de las uniones UIKit eran hermosas, pero no por mucho tiempo.

Limpieza


Otro problema de Combine es que todavía no está completamente claro dónde y cómo liberar recursos en objetos cancelables . Parece que deberíamos almacenarlos en variables de instancia. Pero no recuerdo que en la documentación oficial se dijera algo sobre la limpieza.

RxSwift tiene un DisposeBag terriblemente llamado pero increíblemente conveniente. No es menos fácil crear CancelBag en Combine, pero no estoy muy seguro de que en este caso sea la mejor solución.

En el próximo artículo hablaremos sobre el manejo de errores en RxSwift y Combine, sobre las ventajas y desventajas de ambos enfoques.

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


All Articles