RxSwift y Animaciones en iOS
Publicado: 2022-03-11Si usted es un desarrollador de iOS que ha realizado una cantidad razonable de trabajo de interfaz de usuario y le apasiona, debe amar el poder de UIKit cuando se trata de animaciones. Animar una UIView es tan fácil como un pastel. No tiene que pensar mucho en cómo hacer que se desvanezca, gire, mueva o encoja/expanda con el tiempo. Sin embargo, se complica un poco si desea encadenar animaciones y establecer dependencias entre ellas. Su código puede terminar siendo bastante detallado y difícil de seguir, con muchos cierres anidados y niveles de sangría.
En este artículo, exploraré cómo aplicar el poder de un marco reactivo como RxSwift para hacer que el código se vea mucho más limpio y más fácil de leer y seguir. La idea se me ocurrió cuando estaba trabajando en un proyecto para un cliente. ¡Ese cliente en particular era muy conocedor de la interfaz de usuario (que coincidía perfectamente con mi pasión)! Querían que la interfaz de usuario de su aplicación se comportara de una manera muy particular, con muchas transiciones y animaciones muy elegantes. Una de sus ideas era tener una introducción a la aplicación que contara la historia de qué se trataba la aplicación. Querían que la historia se contara a través de una secuencia de animaciones en lugar de reproducir un video renderizado previamente para que pudiera modificarse y ajustarse fácilmente. RxSwift resultó ser una opción perfecta para el problema como ese, como espero que se dé cuenta una vez que termine el artículo.
Breve introducción a la programación reactiva
La programación reactiva se está convirtiendo en un elemento básico y se ha adoptado en la mayoría de los lenguajes de programación modernos. Hay muchos libros y blogs que explican con gran detalle por qué la programación reactiva es un concepto tan poderoso y cómo ayuda a fomentar un buen diseño de software al hacer cumplir ciertos principios y patrones de diseño. También le brinda un conjunto de herramientas que puede ayudarlo a reducir significativamente el desorden de códigos.
Me gustaría referirme a un aspecto que realmente me gusta: la facilidad con la que puede encadenar operaciones asincrónicas y expresarlas de forma declarativa y fácil de leer.
Cuando se trata de Swift, hay dos marcos en competencia que lo ayudan a convertirlo en un lenguaje de programación reactivo: ReactiveSwift y RxSwift. Usaré RxSwift en mis ejemplos no porque sea mejor sino porque estoy más familiarizado con él. Asumiré que usted, el lector, también está familiarizado con él para que pueda llegar directamente al meollo del asunto.
Encadenamiento de animaciones: a la antigua usanza
Supongamos que desea rotar una vista 180° y luego desvanecerla. Podría hacer uso del cierre de completion
y hacer algo como esto:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } })
Es un poco voluminoso, pero aún así está bien. Pero, ¿qué sucede si desea insertar una animación más en el medio, digamos cambiar la vista a la derecha después de que haya rotado y antes de que desaparezca? Aplicando el mismo enfoque, terminará con algo como esto:
UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0) }, completion: { _ in UIView.animate(withDuration: 0.5, animations: { self.animatableView.alpha = 0 }) }) })
Cuantos más pasos le agregue, más escalonado y engorroso se vuelve. Y luego, si decide cambiar el orden de ciertos pasos, tendrá que realizar una secuencia no trivial de cortar y pegar, que es propensa a errores.
Bueno, Apple obviamente ha pensado en eso: ofrecen una mejor manera de hacerlo, utilizando una API de animaciones basada en fotogramas clave. Con ese enfoque, el código anterior podría reescribirse así:
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: [], animations: { UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }) UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: { self.animatableView.frame = self.animatableView.frame.offsetBy(dx: 50, dy: 0) }) UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.34, animations: { self.animatableView.alpha = 0 }) })
Esa es una gran mejora, con las ventajas clave que son:
- El código se mantiene plano independientemente de cuántos pasos le agregues.
- Cambiar el orden es sencillo (con una advertencia a continuación)
La desventaja de este enfoque es que debe pensar en términos de duraciones relativas y se vuelve difícil (o al menos no muy sencillo) cambiar el tiempo absoluto o el orden de los pasos. Solo piense en los cálculos que tendría que realizar y qué tipo de cambios tendría que hacer en la duración general y las duraciones/horas de inicio relativas para cada una de las animaciones si decidiera hacer que la vista se desvaneciera en 1 segundo en lugar de medio segundo. segundo manteniendo todo lo demás igual. Lo mismo ocurre si desea cambiar el orden de los pasos; tendría que volver a calcular sus tiempos de inicio relativos.
Dadas las desventajas, no encuentro que ninguno de los enfoques anteriores sea lo suficientemente bueno. La solución ideal que estoy buscando debe satisfacer los siguientes criterios:
- El código tiene que permanecer plano independientemente del número de pasos.
- Debería poder agregar/quitar o reordenar fácilmente las animaciones y cambiar su duración de forma independiente sin efectos secundarios en otras animaciones.
Encadenamiento de animaciones: al estilo RxSwift
Descubrí que usando RxSwift, puedo lograr fácilmente ambos objetivos. RxSwift no es el único marco que podría usar para hacer algo así: cualquier marco basado en promesas que le permita envolver operaciones asíncronas en métodos que se pueden encadenar sintácticamente sin hacer uso de bloques de finalización servirá. Pero RxSwift tiene mucho más que ofrecer con su variedad de operadores, de los que hablaremos un poco más adelante.
Aquí está el esquema de cómo voy a hacer eso:
- Envolveré cada una de las animaciones en una función que devuelva un observable de tipo
Observable<Void>
. - Ese observable emitirá solo un elemento antes de completar la secuencia.
- El elemento se emitirá tan pronto como se complete la animación envuelta por la función.
- Encadenaré estos observables usando el operador
flatMap
.
Así es como pueden verse mis funciones:

func rotate(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func shift(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.frame = view.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(_ view: UIView, duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { view.alpha = 0 }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } }
Y así es como lo puse todo junto:
rotate(animatableView, duration: 0.5) .flatMap { [unowned self] in self.shift(self.animatableView, duration: 0.5) } .flatMap { [unowned self] in self.fade(self.animatableView, duration: 0.5) } .subscribe() .disposed(by: disposeBag)
Sin duda, es mucho más código que en las implementaciones anteriores y puede parecer un poco exagerado para una secuencia de animaciones tan simple, pero la belleza es que se puede ampliar para manejar algunas secuencias de animación bastante complejas y es muy fácil de leer debido a la naturaleza declarativa de la sintaxis.
Una vez que lo domine, puede crear animaciones tan complejas como una película y tener a su disposición una gran variedad de prácticos operadores RxSwift que puede aplicar para lograr cosas que serían muy difíciles de hacer con cualquiera de los enfoques antes mencionados.
Así es como podemos usar el operador .concat
para hacer que mi código sea aún más conciso: la parte donde las animaciones se encadenan:
Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Puede insertar retrasos entre animaciones como esta:
func delay(_ duration: TimeInterval) -> Observable<Void> { return Observable.of(()).delay(duration, scheduler: MainScheduler.instance) } Observable.concat([ rotate(animatableView, duration: 0.5), delay(0.5), shift(animatableView, duration: 0.5), delay(1), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Ahora, supongamos que queremos que la vista gire una cierta cantidad de veces antes de comenzar a moverse. Y queremos ajustar fácilmente cuántas veces debe girar.
Primero crearé un método que repita la animación de rotación continuamente y emita un elemento después de cada rotación. Quiero que estas rotaciones se detengan tan pronto como se elimine el observable. Podría hacer algo como esto:
func rotateEndlessly(_ view: UIView, duration: TimeInterval) -> Observable<Void> { var disposed = false return Observable.create { (observer) -> Disposable in func animate() { UIView.animate(withDuration: duration, animations: { view.transform = view.transform.rotated(by: .pi/2) }, completion: { (_) in observer.onNext(()) if !disposed { animate() } }) } animate() return Disposables.create { disposed = true } } }
Y luego mi hermosa cadena de animaciones podría verse así:
Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
Verá lo fácil que es controlar cuántas veces girará la vista: simplemente cambie el valor pasado al operador de take
.
Ahora, me gustaría llevar mi implementación un paso más allá al envolver cada una de las funciones de animación que creé en la extensión "Reactiva" de UIView (accesible a través del sufijo .rx
). Esto lo haría más similar a las convenciones de RxSwift, donde generalmente se accede a las funciones reactivas a través del sufijo .rx
para dejar en claro que están devolviendo un observable.
extension Reactive where Base == UIView { func shift(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.frame = self.base.frame.offsetBy(dx: 50, dy: 0) }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func fade(duration: TimeInterval) -> Observable<Void> { return Observable.create { (observer) -> Disposable in UIView.animate(withDuration: duration, animations: { self.base.alpha = 0 }, completion: { (_) in observer.onNext(()) observer.onCompleted() }) return Disposables.create() } } func rotateEndlessly(duration: TimeInterval) -> Observable<Void> { var disposed = false return Observable.create { (observer) -> Disposable in func animate() { UIView.animate(withDuration: duration, animations: { self.base.transform = self.base.transform.rotated(by: .pi/2) }, completion: { (_) in observer.onNext(()) if !disposed { animate() } }) } animate() return Disposables.create { disposed = true } } } }
Con eso, puedo juntarlos así:
Observable.concat([ animatableView.rx.rotateEndlessly(duration: 0.5).take(5), animatableView.rx.shift(duration: 0.5), animatableView.rx.fade(duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)
A dónde ir desde aquí
Como ilustra este artículo, al liberar el poder de RxSwift, y una vez que tenga sus primitivas en su lugar, puede divertirse mucho con las animaciones. Su código es limpio y fácil de leer, y ya no parece "código": ¡usted "describe" cómo se ensamblan sus animaciones y simplemente cobran vida! Si desea hacer algo más elaborado que lo que se describe aquí, ciertamente puede hacerlo agregando más primitivos propios que encapsulen otros tipos de animaciones. También puede aprovechar siempre el arsenal cada vez mayor de herramientas y marcos desarrollados por algunas personas apasionadas en la comunidad de código abierto.
Si es un converso de RxSwift, siga visitando el repositorio de RxSwiftCommunity regularmente: ¡siempre puede encontrar algo nuevo!
Si está buscando más consejos de interfaz de usuario, intente leer Cómo implementar un diseño de interfaz de usuario de iOS perfecto para píxeles por el compañero Toptaler Roman Stetsenko.
Notas de origen (Comprensión de los conceptos básicos)
- Wikipedia - Swift (lenguaje de programación)
- Lynda.com - RxSwift: patrones de diseño para desarrolladores de iOS
- We Heart Swift - Fundamentos de UIView
- Angular - Observables