Saltar al contenido

Hace unos días os contábamos los fundamentos de RxSwift, pues bien, después de toda esta teoría, vamos con la práctica donde se va a entender mucho mejor todo lo explicado anteriormente.

Para poder utilizar Rx en Swift, hay que importar 2 librerías, y en este ejemplo, lo vamos a hacer con cocoapods:



Una vez instalado los pods, procedemos a poner un ejemplo de utilizar Rx en la parte de la interfaz (sin Drivers). Para ello construiremos el siguiente ejemplo con una tabla y un buscador:



Y el código correspondiente seria:



La librería RxSwift añade extensiones para prácticamente cualquier elemento UIView.

La propiedad text del searchBar emite señales cada vez que dicho texto es modificado. Sobre esta señal se encadena la propiedad orEmpty que fuerza que la variable text  sea un no opcional.

En este momento se encadena la función debounce que recibe un delay sobre cuando lanzar las señales y un Scheduler que indica sobre qué hilo se debe ejecutar dicha señal (en este caso el hilo principal). Aplicar esta función provoca que se lancen eventos cada 0.5 segundos, esto conlleva a tener una sobrecarga de emisiones cuando no es siempre necesario (por ejemplo cuando el campo texto no se modifica), para ello se encadena la función distinctUntilChanged() que provocará que sólo se realicen emisiones cuando el texto se modifique.

En este punto ya tenemos configurada la señal y podemos suscribirnos, para ello utilizamos la función subscribe. En este caso dicha función recibe una cadena con el texto modificado (de tipo Event) que utilizaremos para realizar un filtro sobre los colores. El resultado de este filtro contendrá todos los valores que empiecen por dicha cadena y para actualizar la vista recargaremos el TableView.

Para liberar correctamente todos los recursos usados y eliminar referencias a objetos antiguos una vez se ha ejecutado la suscripción se debe utilizar una DisposableBag. La forma para añadir una DisposableBag a nuestra cadena de llamadas es llamar a la función de liberación de recursos (disposed(by: )) con una instancia de dicha clase.

Pues en el anterior ejemplo, sin darnos cuenta, se produce un “retain cycle”, ya que se está llamando al “self” dentro de la implementación del “onNext” (en nuestro ejemplo sería el Event del subscribe). Y XCode no te avisa de ello, por lo que como dije en los Contras, hay que tener mucho cuidado, y poner lo siguiente:

[weak self]



Pongamos otro ejemplo de Rx, pero ahora orientado a pedir datos a un repositorio.

Imaginemos que tenemos un caso de uso (capa Domain), el cual se encarga de pedir a un WS, y después, guardar en preferencias ciertos datos de ese usuario. Pues quedaría de esta forma:

Podemos ver que tiene 2 instancias de las interfaces de 2 repositorios (LdaRepository y UserDefaultsRepository), cuya implementación está en la capa Data (esto ya es mas de clean architecture).

En el método handle() vemos que llama a la función getDataClient() de ldaRepository, que es un WS, cuya función devuelve también un observable:

Pues para tratar esos datos que me vienen del Observable de la función getDataClient, que me devuelve un Observable de tipo ClientDomainModel, hacemos un flatMapLatest. Con esto conseguimos “desempaquetar” el objeto ClientDomainModel del observable, y podremos utilizarlo para lo que queramos.

En el ejemplo lo utilizamos para obtener un String con los nombres y apellidos, los cuales almacenaremos en UserDefaults (a través de otro repositorio).

**Hemos implementado todos los repositorios con Observables, para tener una uniformidad.**

Con los flatmaps SIEMPRE hay que devolver otro observable, por ello al final del primer flatMap, hago un Observable.just().

La funcionalidad .just me crea un Observable automáticamente, e invoca al .onNext del mismo con lo que le pase por parámetro. Es decir, que el programador no tiene que hacer el onNext y el onComplete, si no que lo hace internamente.

¿Por qué hay 2 flatMapLatest? Pues porque al fin y al cabo, los casos de uso hay lógica de tratamiento de datos entre repositorios, pero hay que devolver algún valor a la Vista/ViewModel, y en este caso hay que devolver un String (handle()-> Observable<String>).

Pues en el siguiente flatMapLatest devuelvo el String que quiera, que en el ejemplo hemos sacado el nombre, y le hemos concatenado “Prueba2”.

Si no fuera necesario tratar mas datos, pues no es necesario poner mas flatMaps.

¿Pero y donde esta el tema de hilos? Nuestro caso de uso, extiende de un Base, el cual implementa todo el tema de hilos, para así no tener duplicidad de código en cada UseCase:

El método subscribeOn sirve para indicar en qué hilo se va a ejecutar el código que hay en el caso de uso.

El método observeOn sirve para indicar en qué hilo va a recibir la información del UseCase (ViewModel en nuestro caso)

Bien, pues si mas o menos tenemos claro esto, ahora ya solo queda llamar al UseCase, que en nuestro ejemplo, lo llamaremos desde el ViewModel:

Aquí hay 2 maneras de recibir el evento:

  1. Bien por un subscriber en el viewModel, que reciba el modelo de Domain, y actualice la vista mediante una interfaz de Output que implemente la vista:

  1. O bien por Drivers (Rx a nivel de vista), que ya puestos, os voy a dar este ejemplo con Drivers:

Pues siguiendo la forma con Drivers, nuestros Input y Output son 2 estructuras creadas en el ViewModel, los cuales sirven para comunicarse con la vista.

Nuestro input recibe un Driver del método de viewWillAppear de la vista, por lo que haremos un flatMapLatest para añadirle una implementación cuando se llame a este método.

En esta implementación llamaremos a nuestro caso de uso, el cual nos devolverá un objeto ClientDomainModel, y que querremos castear a un objeto de vista, por lo que le hacemos un .map (con el .map NO hay que devolver un Observable) para transformarlo al objeto ClientModel:



Si nos hemos fijado, después del .execute (implementado en el BaseUseCase), hay un .asDriverOnErrorJustComplete(). Esto me transforma el Observable del UseCase en un Driver.

Por lo que al final en la variable client será de tipo Driver<ClientModel>, que es justo lo que pide el Output.

¿Y porque no se me ejecuta el código todavía del UseCase? No se ejecuta todavía porque no hay NADIE suscrito. Es la vista quien debe suscribirse a los Drivers, entonces vamos a ver como lo hace:

Aquí vemos en la función bindViewModel(), la cual la hemos llamado en el viewDidLoad, por ejemplo, que transforma el viewWillAppear a Driver, y crea el Input con ese parámetro.

Y al recibir el output, accedemos a la propiedad client, que era de tipo Driver<ClientModel>, y nos suscribimos con la propiedad .drive(), y finalmente añadimos el .disposed, para que, si se destruye la vista, se destruya también esta suscripción.

Dentro del .drive, hemos añadido una variable “clientBinding” de tipo Binder<ClientModel>. Esta sería la manera de “desempaquetar” nuestro ClientModel del Driver.

Con esto sería todo sobre Rx, explicado de la forma mas breve posible que he podido.

 

Wiki

Recomiendo leer este post, para saber como usar flatmaps, maps, etc…

https://medium.com/@abhimuralidharan/higher-order-functions-in-swift-filter-map-reduce-flatmap-1837646a63e8

 

Conclusión

Se que todavía me queda mucho que explicar, pero tendría que extenderme muchísimo mas, pero resumiendo:

  • A nivel de Domain -> Data recomiendo 100% utilizar Rx, ya que facilita muchísimo al programador la gestión de hilos y errores.
  • A nivel de vista, no lo recomiendo tanto si el programador no tiene muy claro todavía tanto los conceptos necesarios, como la metodología a usar.
 Jorge Enrique Guilabert
Jorge Enrique Guilabert

Programador de Desarrollo Mobile iOS/Android nativo en BABEL.

logo linkedin compartir en Linkedin Contacto

Otros artículos destacados