RxSwift e animações no iOS

Publicados: 2022-03-11

Se você é um desenvolvedor iOS que fez uma quantidade razoável de trabalho de interface do usuário e é apaixonado por isso, você deve amar o poder do UIKit quando se trata de animações. Animar um UIView é tão fácil quanto um bolo. Você não precisa pensar muito sobre como fazê-lo desaparecer, girar, mover ou encolher/expandir ao longo do tempo. No entanto, fica um pouco complicado se você quiser encadear animações e configurar dependências entre elas. Seu código pode acabar sendo bastante detalhado e difícil de seguir, com muitos fechamentos aninhados e níveis de recuo.

Neste artigo, explorarei como aplicar o poder de uma estrutura reativa, como RxSwift, para tornar esse código muito mais limpo e mais fácil de ler e seguir. A ideia surgiu quando eu estava trabalhando em um projeto para um cliente. Esse cliente em particular era muito experiente em interface do usuário (o que combinava perfeitamente com minha paixão)! Eles queriam que a interface do usuário de seu aplicativo se comportasse de uma maneira muito particular, com muitas transições e animações muito elegantes. Uma de suas ideias era ter uma introdução ao aplicativo que contaria a história do que era o aplicativo. Eles queriam que a história fosse contada por meio de uma sequência de animações, em vez de reproduzir um vídeo pré-renderizado, para que pudesse ser facilmente ajustado e ajustado. O RxSwift acabou sendo uma escolha perfeita para o problema como esse, como espero que você perceba quando terminar o artigo.

Breve introdução à programação reativa

A programação reativa está se tornando um grampo e foi adotada na maioria das linguagens de programação modernas. Existem muitos livros e blogs explicando detalhadamente por que a programação reativa é um conceito tão poderoso e como ela ajuda a incentivar um bom design de software, reforçando certos princípios e padrões de design. Ele também fornece um kit de ferramentas que pode ajudá-lo a reduzir significativamente a desordem de código.

Eu gostaria de tocar em um aspecto que eu realmente gosto - a facilidade com que você pode encadear operações assíncronas e expressá-las de forma declarativa e fácil de ler.

Quando se trata de Swift, existem dois frameworks concorrentes que ajudam você a transformá-lo em uma linguagem de programação reativa: ReactiveSwift e RxSwift. Usarei o RxSwift em meus exemplos não porque seja melhor, mas porque estou mais familiarizado com ele. Presumo que você, leitor, também esteja familiarizado com isso, para que eu possa ir diretamente ao cerne disso.

Animações de encadeamento: o jeito antigo

Digamos que você queira girar uma vista em 180° e depois esmaecê-la. Você pode usar o encerramento de completion e fazer algo assim:

 UIView.animate(withDuration: 0.5, animations: { self.animatableView.transform = CGAffineTransform(rotationAngle: .pi/2) }, completion: { _ in UIView.animate(withDuration: 0.5) { self.animatableView.alpha = 0 } }) 

GIF animado de um quadrado giratório

É um pouco volumoso, mas ainda bem. Mas e se você quiser inserir mais uma animação no meio, digamos, mude a visualização para a direita depois que ela girar e antes que ela desapareça? Aplicando a mesma abordagem, você terminará com algo assim:

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

GIF animado do retângulo girando, pausando e deslizando para a direita

Quanto mais etapas você adiciona a ele, mais desconcertado e complicado ele fica. E então, se você decidir alterar a ordem de certas etapas, terá que executar uma sequência de recortar e colar não trivial, que é propensa a erros.

Bem, a Apple obviamente pensou nisso - eles oferecem uma maneira melhor de fazer isso, usando uma API de animações baseada em quadros-chave. Com essa abordagem, o código acima poderia ser reescrito assim:

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

Essa é uma grande melhoria, com as principais vantagens sendo:

  1. O código permanece plano, independentemente de quantas etapas você adicionar a ele
  2. Alterar a ordem é simples (com uma ressalva abaixo)

A desvantagem dessa abordagem é que você precisa pensar em termos de durações relativas e torna-se difícil (ou pelo menos não muito simples) alterar o tempo absoluto ou a ordem das etapas. Basta pensar em quais cálculos você teria que fazer e que tipo de mudanças você teria que fazer na duração geral e nas durações relativas/tempos de início para cada uma das animações se você decidisse fazer a visualização desaparecer em 1 segundo em vez de meio segundo, mantendo todo o resto igual. O mesmo vale se você quiser alterar a ordem das etapas - você teria que recalcular seus tempos de início relativos.

Dadas as desvantagens, não acho que nenhuma das abordagens acima seja boa o suficiente. A solução ideal que procuro deve satisfazer os seguintes critérios:

  1. O código deve permanecer plano, independentemente do número de etapas
  2. Devo ser capaz de adicionar/remover ou reordenar animações facilmente e alterar suas durações de forma independente, sem efeitos colaterais para outras animações

Animações de encadeamento: o caminho RxSwift

Descobri que usando o RxSwift, posso facilmente atingir esses dois objetivos. O RxSwift não é o único framework que você pode usar para fazer algo assim - qualquer framework baseado em promessas que permite envolver operações assíncronas em métodos que podem ser encadeados sintaticamente sem usar blocos de conclusão. Mas o RxSwift tem muito mais a oferecer com sua variedade de operadores, sobre os quais falaremos um pouco mais tarde.

Aqui está o esboço de como eu vou fazer isso:

  1. Vou envolver cada uma das animações em uma função que retorna um observável do tipo Observable<Void> .
  2. Esse observável emitirá apenas um elemento antes de completar a sequência.
  3. O elemento será emitido assim que a animação envolvida pela função for concluída.
  4. Vou encadear esses observáveis ​​usando o operador flatMap .

É assim que minhas funções podem ficar:

 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() } }

E aqui está como eu coloquei tudo 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)

Certamente é muito mais código do que nas implementações anteriores e pode parecer um pouco exagerado para uma sequência tão simples de animações, mas a beleza é que pode ser estendido para lidar com algumas sequências de animação bastante complexas e é muito fácil de ler devido a a natureza declarativa da sintaxe.

Depois de entender isso, você pode criar animações tão complexas quanto um filme e ter à sua disposição uma grande variedade de operadores RxSwift úteis que você pode aplicar para realizar coisas que seriam muito difíceis de fazer com qualquer uma das abordagens mencionadas.

Aqui está como podemos usar o operador .concat para tornar meu código ainda mais conciso - a parte em que as animações estão sendo encadeadas:

 Observable.concat([ rotate(animatableView, duration: 0.5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

Você pode inserir atrasos entre animações 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)

Agora, vamos supor que queremos que a vista gire um certo número de vezes antes de começar a se mover. E queremos ajustar facilmente quantas vezes ele deve girar.

Primeiro vou fazer um método que repete a animação de rotação continuamente e emite um elemento após cada rotação. Quero que essas rotações parem assim que o observável for eliminado. Eu poderia fazer algo assim:

 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 } } }

E então minha linda cadeia de animações poderia ficar assim:

 Observable.concat([ rotateEndlessly(animatableView, duration: 0.5).take(5), shift(animatableView, duration: 0.5), fade(animatableView, duration: 0.5) ]) .subscribe() .disposed(by: disposeBag)

Você vê como é fácil controlar quantas vezes a visualização irá girar - basta alterar o valor passado para o operador take .

GIF animado da animação melhorada

Agora, gostaria de levar minha implementação um passo adiante, envolvendo cada função de animação que criei na extensão “Reactive” de UIView (acessível por meio do sufixo .rx ). Isso o tornaria mais próximo das convenções RxSwift, onde as funções reativas são geralmente acessadas por meio do sufixo .rx para deixar claro que estão retornando um observável.

 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 } } } }

Com isso, posso colocá-los juntos assim:

 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)

Para onde ir a partir daqui

Como este artigo ilustra, ao liberar o poder do RxSwift, e uma vez que você tenha seus primitivos no lugar, você pode se divertir muito com as animações. Seu código é limpo e fácil de ler, e não parece mais “código” – você “descreve” como suas animações são montadas e elas simplesmente ganham vida! Se você quiser fazer algo mais elaborado do que o descrito aqui, você certamente pode fazer isso adicionando mais primitivos de sua preferência encapsulando outros tipos de animações. Você também pode sempre aproveitar o arsenal cada vez maior de ferramentas e estruturas desenvolvidas por algumas pessoas apaixonadas na comunidade de código aberto.

Se você é um convertido RxSwift, continue visitando o repositório RxSwiftCommunity regularmente: você sempre pode encontrar algo novo!

Se você está procurando mais conselhos de interface do usuário, tente ler Como implementar um design de interface do usuário iOS perfeito para pixels, do colega Toptaler Roman Stetsenko.


Notas de origem (compreendendo o básico)

  • Wikipedia - Swift (linguagem de programação)
  • Lynda.com - RxSwift: padrões de design para desenvolvedores iOS
  • We Heart Swift - Fundamentos do UIView
  • Angular - Observáveis