Customizando transições entre as telas de um fluxo no iOS da Cora
Continuando a série sobre nosso projeto, um item bem interessante que temos no app iOS da Cora, é a transição entre as telas dos fluxos.
Desde o princípio focamos em desenvolver uma boa estrutura de interface, facilitando o uso, a criação e a manutenção de elementos/componentes de UI e, ao falarmos de UI não poderíamos deixar de fora a possibilidade de customizar nossas transições para que fosse possível criar uma experiência única para todo o app e que fosse de encontro à nossa identidade.
Dentro do desenvolvimento iOS, temos diversas bibliotecas que fornecem uma boa variedade de transições customizadas, como o Hero por exemplo. Porém, dentro do nosso projeto temos uma premissa que é desenvolver tudo dentro de casa. Isso além de nos dar a liberdade e a agilidade de mudar, também permite que a gente não se prenda a um escopo limitado de interações.
Temos um trade off inicial de definição e implementação, mas os benefícios que hoje temos por conta disso são inifinitos.
O que a Apple oferece de recurso pra criarmos nossas transições?
Transition Delegate
Existe um recurso pouco falado, que provê tudo o que precisamos para customizar nossas transições. Mas antes de entrar no detalhe mais técnico, vamos entender como o ciclo de apresentação funciona:
De forma macro, ao iniciar a apresentação de uma UIViewController
, o iOS percorre o seguinte caminho:
Este processo é o que chamamos de ciclo de inicialização e ciclo de apresentação.
Ao executar o método viewWillAppear
, o sistema dá início ao ciclo de apresentação. É neste momento que aquela transição que conhecemos bem ocorre:
- PresentViewController: Transição vertical, de baixo para cima ao entrar e de cima para baixo ao sair;
- PushViewController: Transição horizontal, da direita para a esquerda ao entrar e da esquerda para a direita ao sair.
Podemos explorar também alguns recursos da própria UIViewController, ajustando as propriedades
modalPresentationStyle
, oumodalTransitionStyle
para modificar alguns comportamentos dessas transições, mas isso é assunto pra outro post 😄
Dentro desse fluxo, podemos dizer ao sistema se queremos manter o comportamento padrão, ou se vamos assumir o controle da transição através de um objeto de animação. Então, antes que o sistema execute de fato a transição, uma verificação é feita. Esta verificação serve pra saber se um objeto de animação foi definido. Caso não, a transição padrão é utilizada.
Mas o que é esse tal objeto de animação?
UIViewControllerAnimatedTransitioning
Objeto de animação é um objeto que implementa o protocolo UIViewControllerAnimatedTransitioning
.
Este protocolo possui um contrato com a definição dos métodos necessários para que o app cuide da transição e apenas avise ao sistema quanto tempo será necessário para a animação e se a transição foi finalizada, encerrando assim o ciclo de apresentação.
Consolidando a teoria
Ao solicitar a apresentação de uma UIViewController
, seja por present, ou push, o sistema verifica se existe um objeto de animação. Se existir um objeto de animação, ele será utilizado e o objeto de animação vai determinar quando a transição irá terminar. Se não houver um objeto de animação, o sistema vai utilizar a transição padrão.
Legal, mas e o código?
Clone nosso repo open source e navegue até a pasta TransitionDelegate:
https://github.com/corabank/ios-community.git
Nela, você vai encontrar dois projetos: Start e Final.
Abrindo o projeto Start, no project navigator você vai encontrar uma pasta chamada Views. Nela, 3 pastas com respectivas view controllers:
No sample também você vai ver uma pasta Design System com algumas implementações base pra facilitar a estrutura de layout, mas para o nosso contexto é irrelevante. Aproveito pra pedir que releve o layout 😅
Dando uma passada rápida pelo SceneDelegate, podemos observar que na linha 11 definimos a FlowNavigationController como rootViewController do nosso projeto:
window?.rootViewController = FlowNavigationController(rootViewController: DashViewController())
Inicialmente vamos trabalhar a transição nos dois tipos padrão de apresentação: present e push.
Ao rodar o Sample, podemos ver uma tela inicial com algumas informações e também um rightBarButtonItem
que leva para a AccountViewController.
UIViewControllerAnimatedTransitioning
A primeira coisa a se fazer, é criar nosso objeto de animação. Dentro da pasta DesignSystem, vamos criar um grupo chamado Transition. Nele, vamos criar um novo arquivo chamado CustomTransition e adicionar a implementação:
O NSObject é apenas pra entrar em conformidade com o protocolo de transição
O primeiro método, é onde iremos dizer quanto tempo vamos precisar pra executar a animação por completo.
Já o método animateTransition é onde vamos construir a animação utilizando as informações que chegam no transitionContext.
Para o primeiro método, é uma boa prática criar uma constante que irá conter o tempo da transição. Assim, conseguimos aumentar ou reduzir o tempo facilmente, sem precisar alterar em muitos lugares:
Com o tempo de transição definido, vamos implementar uma animação básica só para entender como o ciclo funciona. A partir daí podemos evoluir e acrescentar alguns detalhes interessantes:
Aqui já conseguimos ter uma ideia do que é o transitionContext
. Quando o sistema passa o controle da transição para o nosso objeto de animação, ele encapsula algumas informações no objeto transitionContext, como a ViewController de destino e a ViewController de partida.
Com estas duas propriedades definidas, utilizamos a ViewController de destino para recuperar qual o frame final desta ViewController.
Neste momento, o frame da ViewController já está definido
Também vamos precisar do containerView que vem no transitionContext. Esta propriedade é a view onde a animação irá performar.
Vamos fazer implementar uma animação de fade in/fade out, bem simples:
Por enquanto ainda não estamos utilizando o fromViewController e o finalFrame, então você não precisa adicionar neste momento. Olharemos isso mais pra frente
Nesta implementação, primeiro alteramos a propriedade alpha
da toViewController.view para 0.
Logo em seguida, adicionamos a toViewController.view no container de animação.
Após isso, implementamos a animação utilizando o método animate do UIView. Note que na duração estamos passando o retorno do primeiro método que implementamos, o transitionDuration.
No bloco de animação, apenas alteramos o valor de alpha
para 1.
Já no bloco de completion, avisamos o sistema através do transitionContext.completeTransition que a animação foi finalizada. No método precisamos passar um valor booleano, sendo true caso a animação tenha sido finalizada, ou false se por qualquer motivo a animação não foi finalizada.
Por isso, além do valor que recebemos no completion através do alias isFinish, utilizamos também a propriedade transitionWasCancelled
do transitionContext que vai dizer se por qualquer motivo o ciclo de transição foi cancelado.
Bom, se você rodar o sample agora, vai notar que a transição continua da mesma forma.
Isso porque precisamos conhecer e implementar mais algumas coisas.
Transition Delegate
Com nosso objeto de animação definido, o próximo passo é implementar o Transition Delegate. Este objeto, que implementa o protocolo UIViewControllerTransitioningDelegate
vai ser o responsável por dizer qual objeto de animação será utilizado para a ação de present e dismiss
Podemos ter mais de um objeto de animação, mas para nosso exemplo, vamos utilizar apenas um.
Crie um novo arquivo chamado CustomTransitionDelegate. Nele, adicione a seguinte implementação:
Aqui, implementamos dois métodos, onde no forPresented
iremos retornar o objeto para a ação de present. No forDismissed
iremos retornar o objeto para a ação de dismiss.
Assim como no objeto de animação, o NSObject é apenas pra entrar em conformidade com o protocolo de transição, agora com o Transition Delegate
Agora, antes de rodar o sample precisamos fazer mais um ajuste. Abra o AccountViewController e antes do viewDidLoad adicione este código:
Primeiro, definimos uma constante que irá segurar a instância do nosso transition delegate customizado.
No init, após o super, atribuímos nosso transition delegate à propriedade transitionDelegate
da ViewController.
Logo em seguida, alteramos a propriedade modalPresentationStyle
para .custom
. Isso irá dizer ao sistema que vamos utilizar uma animação customizada para o ciclo de present/dismiss.
Uma observação interessante é que não fizemos a inicialização do CustomTransitionDelegate diretamente na atribuição do transitionDelegate. Isso porque o transitionDelegate é uma weak var e fazendo a inicialização na atribuição, o delegate é desalocado logo em seguida. Por isso, precisamos garantir que nosso objeto persista durante todo o ciclo de vida da view controller.
Agora sim, podemos rodar o sample e ver o que acontece 😄
Aqui podemos notar duas coisas importantes:
1- A animação que criamos está ocorrendo bem
2- Após o dismiss ser completado, a ViewController de origem é descartada, resultando na window vazia.
O segundo comportamento ocorre por conta de uma última configuração do ciclo de transição que precisamos ajustar.
Na pasta Transitions, crie um arquivo chamado CustomPresentationController e adicione a seguinte implementação:
Não implementamos um init, pois vamos utilizar a implementação padrão do UIPresentationController.
A UIPresentationController
é uma classe importante que controla a aparência e o comportamento de uma view controller ao ser apresentada modally ou como um popover no iOS. Ela oferece uma maneira flexível de personalizar a experiência de apresentação de uma view controller e permite que você crie apresentações visualmente atraentes e interativas em seu aplicativo.
Em nossa implementação, criamos uma subclasse de UIPresentationController e sobrescrevemos a propriedade shouldRemovePresentersView
.
Olhando a descrição na documentação, o entendimento pode ficar um tanto confuso, pois lá informa que esta propriedade retorna um valor booleano que determina se a view do apresentador (a view da view controller que está apresentando a view controller atual) deve ser removida da hierarquia de visualização durante a apresentação modally. O valor padrão desta propriedade, é false. Logo, podemos pensar que, se não queremos que a view controller que está apresentando a view controller atual devemos apenas manter o valor da propriedade como false.
Porém, quando vemos a sessão Discussion da documentação, vemos mais uma informação bem relevante sobre este comportamento. Lá diz que se estivermos implementando uma apresentação que não cubra o conteúdo da view controller por completo, devemos retornar false.
Como em nosso exemplo a view controller de destino cobre por completo a view controller de origem, devemos retornar true.
Agora que temos nosso PresentationController customizado, vamos voltar ao nosso transition delegate customizado e implementar um último método:
O método presentationController
vai dizer ao sistema que no ciclo de transição, iremos utilizar também um Presentation Controller customizado.
Agora sim, podemos rodar o sample e ver que o comportamento indesejado foi ajustado 😅
O projeto com a implementação completa está na pasta Final do nosso repo de exemplo.
Você deve ter notado que em nosso sample ainda temos um TabbarController e um FlowNavigationController. Isso porque ainda temos que explorar o ciclo de transição nestes outros recursos de navegação: A UINavigationController e a UITabBarController.
Mas isso é assunto para o próximo artigo 🤓
Que tal interagir clicando nos claps (de 1 a 50) pra mostrar que curtiu o texto? 👏
Você também pode acessar nossa Página de Carreiras para saber mais sobre nossas vagas e acompanhar a Cora no Linkedin e no Instagram de Corajosers.