
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:
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:
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
?
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 {
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 {
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)
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.