Concrete Logo
Hamburger button

Os detalhes do iOS 11 – View Controller Transitions (Parte 1)

  • Blog
  • 10 de Novembro de 2017
Share

A chegada do iOS 11, algo basicamente simples, mas de extremo bom gosto, me chamou a atenção. A central de controle foi remodelada, não apenas em seu layout e itens disponíveis, mas também a transição da home screen para esse gadget ficou um pouco mais elaborada.

Bem, se você nunca reparou com mais detalhes, vou tentar descrever passo a passo no que acontece.

  1. Um blur effect é adicionado à home screen, gradativamente conforme a central de controle vai surgindo;
  2. A central de controle, com seus itens agrupados, vem de baixo para cima;
  3. Chegando ao final da transição, uma seta se forma na parte superior da tela, finalizando a transição.

Esta simples transição me despertou a curiosidade para um assunto pouco abordado em nosso dia a dia de desenvolvimento. Aliás, se trata de um cuidado simples, mas muito essencial para melhorar o que a Apple sempre (pelo menos até pouco tempo) prezou: User Experience. Aqui eu abro um parênteses bem importante sobre isso…

A responsabilidade de uma boa experiência vai desde quem arquitetou seu aplicativo, até quem implementou a última linha de código. Uma boa experiência de usuário passa pelo UX Designer e pelo desenvolvedor. Passa por todos do time e trabalhar nisso em conjunto é algo que faz uma diferença incrível no resultado final.

Mas vamos ao que realmente interessa.

Como eu posso implementar isso? Como utilizar os recursos do UIKit framework a meu favor?

Vamos começar do começo… Para este post, vamos utilizar o exemplo da transição da central de controles do iOS e criar uma transição “parecida”.

“Parecida”, pois vamos focar em entender a essência de tudo que ocorre por trás de uma transição de tela e não tanto em itens de layout.

Primeiros passos

Faça o download do projeto CustomTransitionViewController que está neste repositório.

Este projeto contém duas Controllers: a inicial e a que vamos apresentar para o usuário.

Ao rodar o projeto pela primeira vez e tocar na tela, vai perceber que o Control Center abre com uma transição comum. Um present view controller básico.

Animated Transitioning

Vamos começar criando uma classe que vai dizer qual o tipo de animação que será feita, uma classe que vai dizer como a transição entre as duas telas vai ocorrer.

Esta classe vai implementar o protocolo UIViewControllerAnimatedTransitioning.

Os métodos nesse protocolo permitem que você defina um “animator object”, que cria as animações para a transição de uma view controller para dentro ou fora da tela em uma quantidade fixa de tempo.

As animações criadas usando esse protocolo, não podem ser interativas — depender da ação do usuário. Para isso, nós combinamos nosso “animator object” com um outro object que controla o tempo das animações. Você vai encontrar mais sobre este assunto, na parte 2 desse post.

Entre na classe criada e implemente dois métodos:

O primeiro método retorna um TimeInterval que diz para o método animateTransition em forma de um “context object” qual o tempo da animação (no caso, estamos dizendo que nossa transição vai demorar 2.5 segundos).

Já o segundo método vai informar ao “context object” qual o escopo de animação que vai ser utilizado.

Vamos começar recuperando três objetos que vamos precisar para animar nossa transição:

  • From View Controller: View controller atual, de onde vai começar;
  • To View Controller: View controller final, onde a transição vai terminar;
  • Final Frame: Frame que a view controller vai assumir quando a transição terminar.

Na assinatura do método, podemos notar que há um “context object”, chamado transitionContext — o protocolo UIViewControllerContextTransitioning.

Este protocolo fornece informações contextualizadas para animações de transição entre view controllers. Durante uma transição, os objetos de animação envolvidos na transição, recebem — do UIKit — uma configuração completa do “context object”.

Adicione o código abaixo dentro do método animateTransition.

Antes de continuar, é importante sabermos como todo o ciclo funciona. Sempre que uma “transition action” é invocada, seja ela presentdismisspush, ou pop, o sistema cria uma área de transição identificada no protocolo transition context pela propriedade containerView.

É importante lembrar que esta propriedade possui um ciclo de vida curto, que dura o tempo que a transição durar. Ou seja, quando a transição termina, o containerView deixa de existir, persistindo apenas a controller apresentada.

Por padrão, a containerView já possui um item: o From View Controller, ponto de start de toda a transição.

A partir daí, vamos adicionar na containerView, todos os elementos que utilizaremos em nossa transição.

Adicione ao método o código abaixo:

  1. Recuperamos o containerView;
  2. Recuperamos as dimensões de tela e definimos um frame para a nossa controller que vai ser apresentada. Perceba que a posição inicial dela em Y será a parte debaixo da tela. Também adicionamos a view da controller à nossa área de transição, a containerView;
  3. Implementamos a animação utilizando o método default da UIView — animate. Nela, definimos que a view atual vai reduzir a opacidade pra 50% e que a nova view vai receber o frame final;
  4. Chamamos o método completeTransition, método do protocolo UIViewControllerContextTransitioning que informa ao “context object” se a transição deve ser finalizada ou não. Para isso, passamos por parâmetro a negativa da propriedade transitionWasCancelled. Esta propriedade informa se a transição foi cancelada. Se ela foi cancelada, por qualquer motivo, a transição não é finalizada e retorna ao seu estado inicial.

Com isso, finalizamos a primeira configuração necessária para customizar nossa transição. Se rodarmos o projeto agora, não vai ocorrer nenhuma mudança, pois a única coisa que fizemos foi estruturar nossa classe de animação.


O segundo passo é criar uma classe que vai ser o “transition delegate” da nossa view controller. Essa classe vai implementar o protocolo UIViewControllerTransitioningDelegate.

Quando você chama qualquer método de transição, “present/push/dimiss/pop view controller”, o UIKit consulta em seu “transition delegate” os objetos que vão ser utilizados para animar a transição da sua view controller.

Transition Delegate

No header da sua classe, crie uma propriedade que vai ser o seu UIViewControllerAnimatedTransitioning e implemente os seguintes métodos:

Em seguida, vamos precisar fazer uma pequena alteração na nossa classe de animação, pois até agora implementamos apenas a animação de entrada da controller, mas não implementamos nenhuma animação de saída. Ou seja, se deixarmos dessa forma, quando chamarmos o “dismiss” da nossa controller que foi carregada, em vez de ela “sair”, a controller anterior vai entrar, como se fosse um present.

Lembre-se: uma vez que você implementou o transitionDelegate da sua controller, todo o fluxo de transição vai passar por ela. Então, você também vai ter que controlar a saída da sua controller, da mesma forma que fez com a entrada.

Volte para a classe de animação que criamos no primeiro passo. Antes de tudo, em vez de deixar a animação no método animateTransition (do protocolo UIViewControllerAnimatedTransitioning) vamos criar dois métodos. Um para controlar a animação de entrada e outro para controlar a animação de saída.

Insira o código abaixo em sua classe:

Não vou entrar no detalhe da implementação, pois o foco é entender o processo de transição entre telas e não a animação em si. Mas agora temos dois novos métodos de animação, um de entrada e um de saída.

Também vamos alterar o método animateTransition, no qual em vez de implementar a animação, ele vai chamar ou o método de present, ou o de dismiss, dependendo do contexto da transição.

Um detalhe importante que podemos notar é que toda controller possui duas propriedades:

  • isBeingPresented: é definida como true quando a controller está sendo apresentada;
  • isBeingDismissed: é definida como true quando a controller está em processo de dismiss.

No caso, vamos utilizar a propriedade isBeingPresented para saber se a controller final (forKey: .to) está sendo apresentada ou se está saindo da tela. Se ela estiver sendo apresentada, vamos chamar o método de present. Caso contrário, chamamos o método que implementa a animação de dismiss.


Com o nosso protocolo de animação implementado e o nosso protocolo de transição também implementado, só falta uma coisa: dizer à View Controller que vai ser apresentada, que ela vai ter que utilizar nosso Custom transition delegate, em vez de usar o delegate default.

Para isso, antes de chamarmos o present(viewController:), vamos adicionar a seguinte linha de código:

Nosso bloco completo vai ficar desta forma:

Por último, mas não menos importante, precisamos “dizer” ao nosso transition delegate que nosso presenter style é modal. Mas calma, não vamos apenas definir o modalPresentationStyle da nossa controller para .overCurrentContext. Se tratando de view controller transition, qual é o principal comportamento em uma modal transition?

Para entender melhor este conceito, vamos entrar em nossa CustomViewController e no viewDidLoad vamos adicionar self.view.backgroundColor = UIColor.clear. Com isso, nossa view controller vai ser apresentada sem qualquer cor de fundo.

Ao rodar o projeto, vai notar que a transição ocorre normalmente. Entretanto, ao final da transição para a nova view controller, a controller inicial é removida da tela.

Este comportamento é normal, pois como falado lá no começo, todo o fluxo de transição entre view controllers acontece dentro de uma área de transição, a containerView. Quando a transição termina, tudo o que foi adicionado nesta área de transição é removido — inclusive a controller inicial (tratada no contexto de transição como Presenter), ficando apenas a controller de destino, ou seja, a controller que foi apresentada.

O que precisamos fazer é dizer ao transition delegate que não queremos que a/o controller/presenter seja removido ao final da transição.

Para isso, vamos utilizar um carinha que até agora não foi mencionado.

UIPresentationController

Toda View Controller possui uma propriedade chamada presentationController. Esta propriedade possui o presentation controller mais próximo que gerencia a view controller atual.

Se uma view controller ou um de seus antecessores é gerenciada por um presentation controller, então, esta view controller possui esse objeto.

Em outras palavras, toda view controller que é apresentada possui um objeto que gerencia esta apresentação.

Este objeto possui uma propriedade chamada shouldRemovePresentersView, que por default é definida como false. Ou seja, por default, sempre que uma view controller é chamada, o seu presenter ou a view controller que a chamou, é removida.

Então, voltando para o nosso problema, o que precisamos fazer é ir até o objeto que gerencia a apresentação dentro do nosso transition delegate e fazer com que essa propriedade retorne true.

Dando uma olhada nesse objeto, podemos ver que ela só pode ser acessada via get, mas não possui nenhum setter para que possamos alterar o valor.

Porém podemos notar também que por possuir o nível de acesso open, podemos fazer um override da sua implementação.

Crie um novo arquivo uma nova classe que seja sub de UIPresentationController.

Nela, vamos fazer o override da propriedade shouldRemovePresentersViewe fazer ela retornar sempre true.

Agora vamos entrar em nosso custom transition delegate e implementar o seguinte método:

Implementando este método, estamos dizendo ao transition delegate que ele deve utilizar nossa CustomPresentationController (que possui a propriedade shouldRemovePresentersView definida como true), em vez de utilizar a default.

Agora vamos adicionar uma última linha de código em nossa controller inicial, antes do present(viewController:).

Desta forma, quando o fluxo de transição começar, o transition delegate vai saber, através do modalPresentationStyle que, em vez de utilizar o presentation controller default, ele vai olhar para o presentation controller retornado do método presentationController(forPresented…), implementado dentro do nosso custom transition delegate.


Simples, não?

Vamos recapitular o conceito?


  1. Toda view controller possui um transition delegate;
  2. Sempre que eu inicio uma transição, o sistema adiciona uma área de transição na tela e tudo fora deste contexto para;
  3. Para eu customizar uma transição, preciso passar para a minha controller um novo delegate de transição;
  4. Esse delegate deve retornar obrigatoriamente um objeto UIViewControllerAnimatedTransitioning através do método animationController;
  5. Esse objeto (custom) deve obrigatoriamente implementar o método animationController contendo todo o fluxo de animação, saída e entrada;
  6. Não é obrigatório, mas é uma boa prática também retornar um TimeInterval com o tempo que vai durar a transição. Para este exemplo inicial, esse item não é tão relevante, mas quando publicarmos a parte 2 deste post, vai ver que faz toda a diferença e realmente precisamos desse carinha;
  7. Para transições no estilo modal preciso também criar um custom UIPresentationController, na qual0 vou sobrescrever a variável shouldRemovePresentersView, fazendo ela retornar true;
  8. Ao final da transição, todos os itens inseridos dentro da área de transição são removidos. Se eu quiser manter algum item que não pertença nem à controller, nem à transição, mas é relevante para o contexto da tela, eu devo adicioná-lo diretamente na window da minha Application, em vez de adicionar na containerView.

E aí? Bora arrebentar com as transições agora? =D

Dúvidas? Comenta aí! \o/

Carry on!

Manja de desenvolvimento mobile e quer trabalhar em um time ágil de verdade? Vem pra cá!