Primeiros passos com animações UIKit

Roberto Sampaio
My iOS Studies [pt-br]
6 min readMay 2, 2021

Em nosso último estudo, aprendemos como configurar um UITableViewDiffableDataSource. Se você não quer perder esses estudos, você pode conferir eles aqui. Você também pode conferir o projeto base nessa postagem e o código no github. Nós usaremos as branches study02-start e study02-final para toda a nossa implementação nesse estudo.

Aprenderemos como criar animações UIKit. No projeto, eu criei essas abaixo:

A primeira animação está usando a função UIView.animate e a segunda usando transições de UIViewController e a função UIView.animateKeyFrame.

Onde começar?

Primeiro, precisamos entender como animações de UIView funcionam. Vamos dar uma olhada na função UIView.animate.

open class func animate(withDuration duration: TimeInterval, animations: @escaping () -> Void)

A função animate pede por uma duração e uma closure com todas as animações que queremos executar.

Abra um arquivo Playground (File > New > Playground) e adicione o código abaixo pra gente ver como isso funciona.

import UIKit
import PlaygroundSupport
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))PlaygroundPage.current.liveView = containerViewlet view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = .green
containerView.addSubview(view)

Se você executar o Playground, vai ver um quadrado branco contendo um quadrado verde. Queremos criar uma animação que será o quadrado verde se movendo dentro do quadrado branco.

Para criar essa animação, precisamos chamar a função animate e atualizar o frame do quadrado verde. Adicione esse código no final.

UIView.animate(withDuration: 1) {
view.frame = CGRect(x: 0, y: 100, width: 200, height: 200)
}

O quadrado verde se moveu a metade do caminho para baixo.

Se queremos mudar o tamanho, podemos alterar a largura e a altura.

Altere o código da animação para o código abaixo:

UIView.animate(withDuration: 1) {
view.frame = CGRect(x: 0, y: 100, width: 100, height: 250)
}

Agora o quadrado verde mudou o seu tamanho.

Acho que você já entendeu como isso funciona! Para animar uma UIView, é necessário chamar a função animate, passando a duração e a closure de animações que vão atualizar as propriedades que queremos alterar. As propriedades vão ser atualizadas gradativamente dentro da duração passada por parâmetro.

Quais propriedades são animáveis?

As propriedades animáveis podem ser categorizadas em:

Posição e tamanho

  • bounds
  • frame
  • center

Transformação

  • rotation
  • scale
  • translation

Aparência

  • backgroundColor
  • alpha

Nós já atualizamos o frame, uma propriedade de posição. Vamos aprender como aplicar transformações.

Transformações são aplicadas por uma struct chamada CGAffineTransform. Para aplicar uma rotação de 45 graus, por exemplo, adicione essa linha ao bloco de animação:

view.transform = CGAffineTransform(rotationAngle: .pi/4)

Para aplicar mais de uma transformação, devemos concatená-las. Por exemplo:

view.transform = 
CGAffineTransform(rotationAngle: .pi/4)
.concatenating(CGAffineTransform(scaleX: 0.5, y: 0.5))

Note que nesse exemplo estamos atualizando o tamanho da view utilizando ambas as propriedades frame e scale. Isso pode ficar um pouco confuso de entender o que está acontecendo e prever o resultado final! Podemos atualizar o tamanho usando apenas uma delas.

Sinta-se livre para praticar com outras propriedades, antes de continuar. É divertido misturar várias delas, atualizar algumas propriedades de aparência e conferir os resultados!

E quanto ao Auto Layout? 🤔

Animações com Auto Layout funcionam um pouco diferente. Vamos adicionar outro código pra ver como funciona.

Esse é o setup que precisamos:

import UIKit
import PlaygroundSupport
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))PlaygroundPage.current.liveView = containerView
let view = UIView()
containerView.addSubview(view)view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
view.widthAnchor.constraint(equalToConstant: 200).isActive = true
view.heightAnchor.constraint(equalToConstant: 200).isActive = true
view.layoutIfNeeded()
view.backgroundColor = .green

Para animar as views usando Auto Layout devemos atualizar as constraints relacionadas, ao invés das propriedades.

Por exemplo, para adicionar uma animação movendo e aumentando o tamanho da view, devemos atualizar as constraints relacionadas ao tamanho e posição. Para fazermos isso, precisamos ter uma referência das constraints e atualizá-las no bloco de animações.

Atualize o código relacionado às constraints de leading e height:

let leadingConstraint = view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor)
leadingConstraint.isActive = true...let heightConstraint = view.widthAnchor.constraint(equalToConstant: 200)
heightConstraint.isActive = true

Agora atualize as constraints na closure de animações.

UIView.animate(withDuration: 3) {
leadingConstraint.constant = 50
heightConstraint.constant = 250
containerView.layoutIfNeeded()
}

Funcionou! 😉

Talvez você esteja se perguntando porque chamamos layoutIfNeeded(). A resposta é porque quando chamamos essa função, estamos forçando a view a atualizar seu layout imediatamente.

Precisamos chamar essa função antes de iniciarmos a animação, após atribuir as constraints, do contrário as constraints atribuídas antes de chamarmos a função de animação serão animadas também. E também chamamos a função no fim da closure para que as animações tenham efeito.

Repare que o Auto Layout é relacionado com a posição e o tamanho da view. Logo, o alpha e o backgroundColor funcionam do jeito que vimos antes.

E as transformações?

Sim, pode ser um pouco diferente do que você estava esperando, mas ainda funciona do mesmo jeito, mesmo com o Auto Layout. De acordo com a documentação da Apple:

In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame.

Tente você mesmo!

Mola com amortecimento

Podemos criar um tipo de animação que é como se fosse uma mola com amortecimento. Só precisamos passar mais três parâmetros na função de animação. Os parâmetros são:

delay: O atraso em segundos até a animação começar.

usingSpringWithDamping: A força do amortecimento da mola. Os valores variam entre 0 e 1, dependendo da força do amortecimento que queremos. Quanto maior, mais forte será o amortecimento.

initialSpringVelocity: O pontapé inicial da animação. Ou seja, a velocidade inicial que ela vai começar a ser executada. Quanto maior, mais velocidade.

Para entender melhor como funciona, seria interessante tentar diferentes valores e ver qual se encaixa melhor no propósito desejado. O melhor que usei foi algo entre 0.4 e 0.6 para usingSpringWithDamping e 10 para initialSpringVelocity.

Animações em sequência

Podemos criar animações em sequência. Para fazer isso, temos a função animateKeyFrames. Nesse caso, temos uma duração para a animação inteira, e as durações relativas de cada quadro da animação, bem como quando cada uma vai começar.

Por exemplo, se queremos mover a view em 0.2 segundos, alterar seu tamanho em 0.2 segundos, rotacionar em 0.3 segundos e voltar tudo ao normal em 0.3 segundos, devemos fazer algo como o código abaixo.

UIView.animateKeyframes(withDuration: 1, delay: 0, options: [], animations: {        UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.2, animations: {
leadingConstraint.constant = 100
view.layoutIfNeeded()
})

UIView.addKeyframe(withRelativeStartTime: 0.2, relativeDuration: 0.2, animations: {
heightConstraint.constant = 100
view.layoutIfNeeded()
})

UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.3, animations: {
view.transform = .init(rotationAngle: .pi/4)
view.layoutIfNeeded()
})
UIView.addKeyframe(withRelativeStartTime: 0.7, relativeDuration: 0.3, animations: {
leadingConstraint.constant = 0
heightConstraint.constant = 200
view.transform = .identity
view.layoutIfNeeded()
})
})

Repare que tudo acontece em um segundo. Se trocarmos para dois segundos, a primeira animação acontecerá em 0.4 segundos, porque é proporcional à duração total. A mesma coisa acontece com as outras animações, em sua duração e momento de início.

Transição entre View Controllers

Podemos criar uma animação para as transições quando um view controller chama present ou dismiss.

Para fazer isso, precisamos criar um tipo que implemente NSObject e UIViewControllerAnimatedTransitioning. Esse será quem fará a animação. E o UIViewController, que será animado, deve implementar o protocolo UIViewControllerTransitioningDelegate.

Para animar o present e/ou o dismiss devemos implementar as respectivas funções:

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?

Elas vão retornar quem vai fazer as animações ao chamarmos present e/ou dismiss.

O tipo UIViewControllerAnimatedTransitioning (quem vai animar) precisa implementar:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeIntervalfunc animateTransition(using transitionContext: UIViewControllerContextTransitioning)

A primeira função deve retornar a duração da transição, e na segunda é onde a mágica acontece.

No transitionContext acessamos o containerView e ambas as views da transição (a que está sendo exibida no momento e a próxima, após a transição). Podemos acessar essas views através dos métodos transitionContext.view(forKey: from) e transitionContext.view(forKey: to).

Basicamente no passo de present, precisamos adicionar a toView à containerView (e adicionar outras view também, se quisermos) e criar as animações. No passo de dismiss, não precisamos adicionar nenhuma das views da transição à containerView (podemos adicionar outras, se quisermos). Só precisamos criar as animações ().

O último passo é chamar transitionContext.completeTransition(true) na closure de completion, senão a transição nunca será finalizada.

Para conferir um exemplo completo disso, sugiro dar uma olhada na branch study02-final. Lá você vai encontrar ambas as animações que eu fiz para estudar esses tópicos.

A animação de troca de listas você vai encontrar na função switchNotesOption. Repare que usei a função snapshotView. Usei isso para simular como se existissem duas tabelas na cena, enquanto na verdade era apenas uma.

Quem faz as animações de transição é o ZoomAnimator e a DiaryViewController implementa o UIViewControllerTransitioningDelegate.

Esse é o final de nosso estudo! Deixe um comentário aqui se você tem quaisquer dúvidas ou sugestões! Seria um prazer receber algum feedback! :)

--

--

Roberto Sampaio
My iOS Studies [pt-br]

Servo de Jesus, aprendendo a escrever, desenvolver software, lutar e tocar guitarra. Aprendendo, sempre.