Customizando transições entre as telas de um fluxo no iOS da Cora

Renato Sarro Matos
Cora
Published in
8 min readJul 23, 2023

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:

Estrutura macro do ciclo de inicialização e apresentação de uma UIViewController

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, ou modalTransitionStyle 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:

Estrutura de pastas do projeto

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.

Transição padrão

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 😄

Transição customizada

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 😅

Transição completa

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.

--

--

Renato Sarro Matos
Cora
Writer for

iOS Lead na Cora, metido a músico e apreciador da arte do tricô