Criar animações é ótimo. Eles são uma parte importante das
Diretrizes da interface humana do
iOS . As animações ajudam a chamar a atenção do usuário para coisas importantes ou simplesmente tornam o aplicativo menos chato.
Existem várias maneiras de implementar animação no iOS. Provavelmente, a maneira mais popular é usar
UIView.animate (withDuration: animations :) . Você pode animar uma camada de imagem usando
CABasicAnimation . Além disso, o UIKit permite configurar animações personalizadas para exibir o controlador usando o
UIViewControllerTransitioningDelegate .
Neste artigo, quero discutir outra maneira interessante de animar visualizações -
UIViewPropertyAnimator . Esta classe fornece muito mais funções de gerenciamento do que seu antecessor,
UIView.animat e. Use-o para criar animações temporárias, interativas e interrompidas. Além disso, é possível alterar rapidamente o animador.
Apresentando o UIViewPropertyAnimator
O UIViewPropertyAnimator foi introduzido no
iOS 10 . Permite criar animações de maneira orientada a objetos. Vejamos um exemplo de uma animação criada usando o
UIViewPropertyAnimator .

Foi assim que aconteceu ao usar o UIView.
UIView.animate(withDuration: 0.3) { view.frame = view.frame.offsetBy(dx: 100, dy: 0) }
E aqui está como fazer isso com o
UIViewPropertyAnimator :
let animator = UIViewPropertyAnimator(duration:0.3, curve: .linear) { view.frame = view.frame.offsetBy(dx:100, dy:0) } animator.startAnimation()
Se você precisar verificar a animação, basta criar um Playground e executar o código como mostrado abaixo. Ambos os fragmentos de código levarão ao mesmo resultado.

Você pode pensar que neste exemplo não há muita diferença. Então, qual é o sentido de adicionar uma nova maneira de criar animações?
O UIViewPropertyAnimator se torna mais útil quando você precisa criar animações interativas.
Animação interativa e interrompida
Você se lembra do gesto clássico "Passe o dedo para desbloquear o dispositivo"? Ou o gesto "Mova o dedo na tela de baixo para cima" para abrir o Centro de Controle? Estes são ótimos exemplos de animação interativa. Você pode começar a mover a imagem com o dedo, soltá-la e a imagem retornará à sua posição original. Além disso, você pode capturar a imagem durante a animação e continuar movendo-a com o dedo.
As animações do UIView não fornecem uma maneira fácil de controlar a porcentagem de conclusão da animação. Você não pode pausar a animação no meio de um loop e continuar sua execução após a interrupção.
Nesse caso, falaremos sobre
UIViewPropertyAnimator . A seguir, veremos como você pode criar facilmente animação totalmente interativa, interrompida e animação reversa em algumas etapas.
Preparação do projeto de lançamento
Primeiro, você precisa
baixar o projeto inicial .
Depois de abrir o arquivo, você encontrará o aplicativo
CityGuide que ajuda os usuários a planejar suas férias. O usuário pode rolar pela lista de cidades e abrir uma descrição detalhada com informações detalhadas sobre a cidade que ele gostou.
Considere o código fonte do projeto antes de começarmos a criar belas animações. Aqui está o que você pode encontrar em um projeto abrindo-o no
Xcode :
- ViewController.swift : o controlador de aplicativo principal com um UICollectionView que exibe uma matriz de objetos City .
- CityCollectionViewCell.swift: célula para exibir a cidade . De fato, neste artigo, a maioria das alterações será aplicada a essa classe. Você pode perceber que descriptionLabel e closeButton já estão definidos na classe. No entanto, após iniciar o aplicativo, esses objetos serão ocultados. Não se preocupe, eles serão visíveis um pouco mais tarde. Essa classe também possui propriedades collectionView e index . Mais tarde, eles serão usados para animação.
- CityCollectionViewFlowLayout.swift: essa classe é responsável pela rolagem horizontal. Nós não vamos mudar ainda.
- City.swift : O modelo principal de aplicativo possui um método usado no ViewController.
- Main.storyboard: Aqui você encontra a interface do usuário para o ViewController e CityCollectionViewCell .
Vamos tentar criar e executar o aplicativo de amostra. Como resultado, obtemos o seguinte.
Implementação de animação de implantação e recolhimento
Depois de iniciar o aplicativo, uma lista de cidades é exibida. Mas o usuário não pode interagir com objetos na forma de células. Agora você precisa exibir informações para cada cidade quando o usuário clica em uma das células. Dê uma olhada na versão final do aplicativo. Aqui está o que realmente precisava ser desenvolvido:
A animação parece boa, não é? Mas não há nada de especial aqui, é apenas a lógica básica do
UIViewPropertyAnimator . Vamos ver como implementar esse tipo de animação. Crie um método
collectionView (_: didSelectItemAt) , adicione o seguinte fragmento de código ao final do arquivo
ViewController :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let selectedCell = collectionView.cellForItem(at: indexPath)! as! CityCollectionViewCell selectedCell.toggle() }
Agora precisamos implementar o método de
alternância . Vamos mudar para
CityCollectionViewCell.swift e implementar esse método.
Primeiro, adicione a enumeração
State na parte superior do arquivo, logo antes da declaração da classe
CityCollectionViewCell . Esta listagem permite rastrear o estado de uma célula:
private enum State { case expanded case collapsed var change: State { switch self { case .expanded: return .collapsed case .collapsed: return .expanded } } }
Adicione algumas propriedades para controlar a animação na classe
CityCollectionViewCell :
private var initialFrame: CGRect? private var state: State = .collapsed private lazy var animator: UIViewPropertyAnimator = { return UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) }()
A variável
initialFrame é usada para armazenar o quadro da célula até que a animação seja
executada .
O estado é usado para rastrear se a célula é expandida ou recolhida. E a variável
animador é usada para controlar a animação.
Agora adicione o método de
alternância e chame-o do método
close , por exemplo:
@IBAction func close(_ sender: Any) { toggle() } func toggle() { switch state { case .expanded: collapse() case .collapsed: expand() } }
Em seguida, adicionamos mais dois métodos:
expand () e
collapse () . Continuaremos sua implementação. Primeiro, começamos com o método
expansiond () :
private func expand() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.initialFrame = self.frame self.descriptionLabel.alpha = 1 self.closeButton.alpha = 1 self.layer.cornerRadius = 0 self.frame = CGRect(x: collectionView.contentOffset.x, y:0, width: collectionView.frame.width, height: collectionView.frame.height) if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x -= 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x += 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = false collectionView.allowsSelection = false default: () } } animator.startAnimation() }
Quanto código. Deixe-me explicar o que está acontecendo passo a passo:
- Primeiro, verifique se collectionView e index não são iguais a zero. Caso contrário, não poderemos iniciar a animação.
- Em seguida, comece a criar a animação chamando animator.addAnimations .
- Em seguida, salve o quadro atual, que é usado para restaurá-lo na animação de convolução.
- Em seguida, definimos o valor alfa para descriptionLabel e closeButton para torná-los visíveis.
- Em seguida, remova o canto arredondado e defina um novo quadro para a célula. A célula será mostrada em tela cheia.
- Em seguida, movemos as células vizinhas.
- Agora chame o método animator.addComplete () para desativar a interação da imagem da coleção. Isso evita que os usuários rolem durante a expansão celular. Altere também o estado atual da célula. É importante alterar o estado da célula e somente depois disso a animação termina.
Agora adicione uma animação de convolução. Em resumo, apenas restauramos a célula ao seu estado anterior:
private func collapse() { guard let collectionView = self.collectionView, let index = self.index else { return } animator.addAnimations { self.descriptionLabel.alpha = 0 self.closeButton.alpha = 0 self.layer.cornerRadius = self.cornerRadius self.frame = self.initialFrame! if let leftCell = collectionView.cellForItem(at: IndexPath(row: index - 1, section: 0)) { leftCell.center.x += 50 } if let rightCell = collectionView.cellForItem(at: IndexPath(row: index + 1, section: 0)) { rightCell.center.x -= 50 } self.layoutIfNeeded() } animator.addCompletion { position in switch position { case .end: self.state = self.state.change collectionView.isScrollEnabled = true collectionView.allowsSelection = true default: () } } animator.startAnimation() }
Agora é hora de compilar e executar o aplicativo. Tente clicar na célula e você verá a animação. Para fechar a imagem, clique no ícone de cruz no canto superior direito.
Adicionando processamento de gestos
Você pode reivindicar atingir o mesmo resultado usando
UIView.animate . Qual é o sentido de usar um
UIViewPropertyAnimator ?
Bem, é hora de tornar a animação interativa. Adicione um
UIPanGestureRecognizer e uma nova propriedade chamada
popupOffset para rastrear quanto a célula pode ser movida. Vamos declarar essas variáveis na classe
CityCollectionViewCell :
private let popupOffset: CGFloat = (UIScreen.main.bounds.height - cellSize.height)/2.0 private lazy var panRecognizer: UIPanGestureRecognizer = { let recognizer = UIPanGestureRecognizer() recognizer.addTarget(self, action: #selector(popupViewPanned(recognizer:))) return recognizer }()
Em seguida, adicione o seguinte método para registrar a definição de furto:
override func awakeFromNib() { self.addGestureRecognizer(panRecognizer) }
Agora você precisa adicionar o método
popupViewPanned para rastrear o gesto de furto. Cole o seguinte código no
CityCollectionViewCell :
@objc func popupViewPanned(recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: toggle() animator.pauseAnimation() case .changed: let translation = recognizer.translation(in: collectionView) var fraction = -translation.y / popupOffset if state == .expanded { fraction *= -1 } animator.fractionComplete = fraction case .ended: animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) default: () } }
Existem três estados. No início do gesto, inicializamos o animador usando o método de
alternância e pausamos imediatamente. Enquanto o usuário arrasta a célula, atualizamos a animação definindo as propriedades do multiplicador de fraçãoCompleto. Essa é a principal mágica do animador, que permite controlar. Finalmente, quando o usuário solta o dedo, o método do animador
continueAnimation é chamado para continuar a animação. Então a célula se moverá para a posição de destino.
Depois de iniciar o aplicativo, você pode arrastar a célula para cima para expandi-la. Em seguida, arraste a célula expandida para baixo para recolhê-la.
A animação agora parece muito boa, mas não é possível interromper a animação no meio. Portanto, para tornar a animação totalmente interativa, você precisa adicionar outra função - interrupção. O usuário pode iniciar a animação de expansão / recolhimento como de costume, mas a animação deve ser pausada imediatamente após o usuário clicar na célula durante o ciclo de animação.
Para fazer isso, você precisa salvar o progresso da animação e levar esse valor em consideração para calcular a porcentagem de conclusão da animação.
Primeiro, declare uma nova propriedade no
CityCollectionViewCell :
private var animationProgress: CGFloat = 0
Atualize o bloco
.began do método
popupViewPanned com a seguinte linha de código para lembrar o progresso:
animationProgress = animator.fractionComplete
No bloco
.changed ,
você precisa atualizar a seguinte linha de código para calcular corretamente a porcentagem de conclusão:
animator.fractionComplete = fraction + animationProgress
Agora o aplicativo está pronto para teste. Execute o projeto e veja o que acontece. Se todas as ações forem executadas corretamente, seguindo minhas instruções, a animação deverá se parecer com:
Animação reversa
Você pode encontrar uma falha para a implementação atual. Se você arrastar a célula um pouco e depois retorná-la à sua posição original, a célula continuará a se expandir quando você soltar o dedo. Vamos corrigir esse problema para tornar a animação interativa ainda melhor.
Vamos atualizar o bloco
.end do método
popupViewPanned , conforme descrito abaixo:
let velocity = recognizer.velocity(in: self) let shouldComplete = velocity.y > 0 if velocity.y == 0 { animator.continueAnimation(withTimingParameters: nil, durationFactor: 0) break } switch state { case .expanded: if !shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } case .collapsed: if shouldComplete && !animator.isReversed { animator.isReversed = !animator.isReversed } if !shouldComplete && animator.isReversed { animator.isReversed = !animator.isReversed } } animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
Agora, levamos em conta a velocidade do gesto para determinar se a animação deve ser revertida.
E, finalmente, insira outra linha de código no bloco
.changed . Coloque esse código à direita do cálculo
animator.fractionComplete .
if animator.isReversed { fraction *= -1 }
Vamos executar o aplicativo novamente. Agora tudo deve funcionar sem falhas.
Corrigir gesto de panorâmica
Portanto, concluímos a implementação da animação usando o
UIViewPropertyAnimator . No entanto, há um erro desagradável. Você pode tê-la conhecido enquanto testava o aplicativo. O problema é que não é possível rolar a célula horizontalmente. Vamos tentar deslizar para a esquerda / direita pelas células e somos confrontados com o problema.
O principal motivo está relacionado ao
UIPanGestureRecognizer que
criamos . Ele também captura um gesto de furto e entra em conflito com o reconhecedor de gestos
interno UICollectionView .
Embora o usuário ainda possa rolar pela parte superior / inferior das células ou pelo espaço entre as células para rolar pelas cidades, ainda não gosto de uma interface do usuário tão ruim. Vamos consertar.
Para resolver conflitos, precisamos implementar um método delegado chamado
gestRecognizerShouldBegin (_ :) . Este método controla se o reconhecedor de gestos deve continuar a interpretar os toques. Se você retornar
falso no método, o reconhecedor de gestos ignorará os toques. Então, o que vamos fazer é dar à nossa própria ferramenta de reconhecimento de panorama a capacidade de ignorar movimentos horizontais.
Para fazer isso, vamos definir o
delegado do nosso reconhecedor de pan. Insira a seguinte linha de código na inicialização do
panRecognizer (você pode colocar o código antes do
reconhecedor de retorno :
recognizer.delegate = self
Em seguida, implementamos o
método gestRecognizerShouldBegin (_ :) da seguinte maneira:
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return abs((panRecognizer.velocity(in: panRecognizer.view)).y) > abs((panRecognizer.velocity(in: panRecognizer.view)).x) }
Abriremos / fecharemos se sua velocidade vertical for maior que a velocidade horizontal.
Uau! Vamos testar o aplicativo novamente. Agora você pode percorrer a lista de cidades deslizando para a esquerda / direita pelas células.
Bônus: Recursos de sincronização personalizada
Antes de terminar este tutorial, vamos falar sobre as funções de temporização. Você ainda se lembra do caso em que o desenvolvedor solicitou a implementação de uma função de sincronização personalizada para a animação que você criou?
Normalmente, você deve alterar o
UIView.animation para
CABasicAnimation ou envolvê-lo na
transação de CAT .
Com o UIViewPropertyAnimator, você pode implementar facilmente funções de tempo personalizadas.
As
funções de tempo (ou funções de facilitação) são entendidas como funções de velocidade da animação que afetam a taxa de alteração de uma ou outra propriedade animada. Atualmente, há quatro tipos suportados: facilidadeInOut, facilidadeIn, facilidadeOut, linear.
Substitua a inicialização do animador por essas funções de tempo (tente desenhar sua própria curva cúbica de Bezier) da seguinte maneira:
private lazy var animator: UIViewPropertyAnimator = { let cubicTiming = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.17, y: 0.67), controlPoint2: CGPoint(x: 0.76, y: 1.0)) return UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTiming) }()
Como alternativa, em vez de usar parâmetros de sincronização cúbica, você também pode usar a sincronização de mola, por exemplo:
let springTiming = UISpringTimingParameters(mass: 1.0, stiffness: 2.0, damping: 0.2, initialVelocity: .zero)
Tente iniciar o projeto novamente e veja o que acontece.
Conclusão
Com o UIViewPropertyAnimator, você pode aprimorar as telas estáticas e a interação do usuário por meio de animações interativas.
Eu sei que você não pode esperar para perceber o que aprendeu em seu próprio projeto. Se você aplicar essa abordagem em seu projeto, será muito legal, deixe-me saber sobre isso, deixando um comentário abaixo.
Como referência, aqui você pode
baixar o rascunho final .
Links adicionais
Animações profissionais usando o UIKit -
https://developer.apple.com/videos/play/wwdc2017/230/Documentação do UIViewPropertyAnimator para desenvolvedores da Apple -
https://developer.apple.com/documentation/uikit/uiviewpropertyanimator