Transições personalizadas no iOS 7 e um pouco sobre UX

Era uma vez, decidi substituir a transição modal padrão por uma personalizada. As transições padrão parecem muito pesadas às vezes. Meu objetivo era manter meu aplicativo como uma grande tela sem limites claros entre os controladores. Eu não planejei fazer uma transição baseada em mola só porque parece legal, eu queria ter uma animação sutil, menos pesada, mais leve para tornar a experiência com o aplicativo mais agradável.

É especialmente importante quando você tem aquele tipo de aplicativo que solicita algum tipo de informação do usuário antes de realmente levá-lo ao ponto do aplicativo, ao conteúdo principal do aplicativo, antes de se tornar útil.

Claro que você otimiza o número de passos para ridículos 3-4, muito curto, com recursos mais avançados ocultos por padrão, com o mínimo de informações necessárias para começar a usar o aplicativo. Mas você ainda tem aquele sentimento pesado sobre alguns controladores surgindo.

Eu fui muito inspirado pelo aplicativo Fonoteka da Zvooq, que infelizmente está disponível apenas na loja russa, mas se você trocar de loja, você pode obtê-lo e está em inglês. Eu vejo isso como um ótimo exemplo de ótima experiência do usuário. A maioria das interações acontecem usando deslizamentos, em todas as 4 direções, e tudo mantido em uma única tela que muda seu plano de fundo de controlador para controlador, é muito suave.

Cenário

Embora eu entenda que essa abordagem não é para todos os aplicativos e nem para o meu aplicativo, eu queria fazer uma mudança em direção a transições menos intrusivas.

É incrível que, desde o iOS 7, você possa implementar seu próprio animador e animar as transições entre os controladores da maneira que quiser. E porque parecia bastante simples de implementar, decidi organizar um dia de hack (ou dois) para ver o potencial. Eu configurei um prazo de dois dias para produzir algo decente, caso contrário, eu simplesmente enterraria outro branch de recursos e esqueceria dele.

No entanto, como de costume, especialmente com a Apple, el Diablo en los Detalles .

Quando tentei implementar minha própria transição, todas as coisas desmoronaram, desde meu código estável, que funcionava perfeitamente antes, até o estilo da barra de status e desdobrar segues. Controladores quentes e sensuais que costumavam surfar na superfície da tela pareciam mais, bem, navios afundando. 🙁

Eu li muito sobre transições personalizadas na web nos últimos dias, verifiquei cada postagem de blog e cada repositório git que pude encontrar e, então, acabo ficando ainda mais frustrado. Porque essencialmente ninguém está fazendo isso direito. Bem, talvez eu também não faça direito, mas meus testes funcionam bem 🙂

E então decidi colocar esta postagem dramática no Coderwall e espero que a maneira de contar essa história seja apropriada o suficiente para estar aqui, pois é um pouco mais do que copiar e colar algum código. Além disso, é provavelmente um dos meus primeiros posts em inglês e eu nunca tive muita paciência e tempo antes, então espero que vocês não me odeiem até o resto da vida e aproveitem essa jornada comigo.

Vamos definir um padrão para uma transição adequada:

  1. Tratamento correto de eventos de aparência (por exemplo, nenhum viewWillAppear duplo e outros semelhantes)
  2. Manuseio correto da aparência da barra de status
  3. Os controladores de navegação devem funcionar e ter uma boa aparência durante a transição

<br/>

Delegado de transição

A primeira coisa com a qual você começa nesse longo caminho é a adoção de UIViewControllerTransitioningDelegateprotocolo em seu controlador de visualização.

Basicamente, este protocolo é um contrato entre o UIKit e seu controlador, então quando o UIKit precisa de um animador para a transição, ele vem ao seu controlador de visualização e pede ajuda.

Você simplesmente cria uma instância do seu animador e a retorna para apresentação e dispensa do controlador de visualização.

Existe a possibilidade de retornar selfe implementar o código de transição no controlador, mas gosto da ideia de separar o animador do controlador de visualização para que ele possa ser reutilizado em outras partes do aplicativo.

@implementation MYViewController (Transitions)

- (id <UIViewControllerAnimatedTransitioning>)
animationControllerForPresentedController
:(UIViewController *)presented
presentingController
:(UIViewController *)presenting
sourceController
:(UIViewController *)source
{
return [MYModalTransitionAnimator new];
}

- (id <UIViewControllerAnimatedTransitioning>)
animationControllerForDismissedController
:(UIViewController *)dismissed
{
return [MYModalTransitionAnimator new];
}

@end

Segue

A boa notícia é que você pode criar segues em IB, a má notícia de que você não pode alterar o estilo de transição para customizado ou definir delegado para transição, então você tem que pegar um segue no código e modificá-lo .prepareForSegue:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"MYSegue"]) {
UIViewController* controller = (UIViewController*)segue.destinationViewController;
controller
.transitioningDelegate = self; // 1
controller
.modalPresentationStyle = UIModalPresentationCustom; // 2
controller
.modalPresentationCapturesStatusBarAppearance = YES; // 3
}
}
  1. Configure o delegado de transição para apresentar o controlador de visualização
  2. Configurar estilo de apresentação modal personalizado
  3. Deixe o controlador apresentado assumir a aparência da barra de status. Isso ajuda a conectar o preferredStatusBarStylemecanismo padrão para o controlador apresentado usando a transição personalizada. Mesmo que a documentação diga que para os controladores de tela cheia você não precisa fazer nada, mas sem esse sinalizador não funcionou para mim.

Esqueleto do animador

Vamos dar uma olhada no esqueleto da classe do animador:

@ interface MYModalTransitionAnimator : NSObject<UIViewControllerAnimatedTransitioning>
@end

@implementation MYModalTransitionAnimator

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.5; // 1
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext // 2
{
UIViewController* destination = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

if([destination isBeingPresented]) { // 3
[self animatePresentation:transitionContext]; // 4
} else {
[self animateDismissal:transitionContext]; // 5
}
}

@end
  1. A duração real da transição, deve corresponder à duração de sua animação CA.

  2. TransitionContext é um objeto opaco que segue um protocolo específico que nos permite acessar algumas das informações úteis de que precisamos para a animação. Como controladores de visualização de origem e destino, visualização de contêiner (que UITransitionViewserá discutido mais tarde) e método que devemos chamar quando nossa animação UIView terminar.transitionComplete:

  3. Algumas pessoas usam o sinalizador para identificar se o animador deve apresentar ou dispensar o controlador. Prefiro consultar o controlador de destino para descobrir o que está acontecendo.

  4. & 5 Eu mantenho as duas animações separadamente em dois métodos diferentes, por conveniência.

<br/>

Como funciona

Portanto, antes de lançar algumas animações, gostaria de me aprofundar um pouco mais no processo de transição para entender melhor o que o UIKit está fazendo nos bastidores.

Portanto, se você já checou a hierarquia de visualização, UIWindowprovavelmente percebeu que no topo da UIWindow temos UITransitionViewe um nível abaixo dela UILayoutContainerView. Sim, essas vistas mágicas só Deus sabe por que colocadas lá.

UITransitionViewé nosso playground e UILayoutContainerViewé um contêiner para a visualização do nosso controlador.

No ponto em que o UIKit chama , tudo está configurado para que possamos executar a animação. O contexto de transição aponta para uma visão de transição onde podemos mexer sem nos queimar. Ele também tem dois de nossos controladores para transição.animateTransition:containerView

Caso a transição aconteça, digamos implicitamente, entre dois controladores de navegação, ele retorna cada um deles, porque UIStoryboardSeguedescobriu isso para nós, então não nos deixará animar de um dos controladores de visualização do controlador de navegação para qualquer outro, mas ao invés ele retornará o próprio controlador de navegação. O que é muito inteligente, caso contrário, sua barra de navegação ficaria no topo e desapareceria de repente.

Então, vamos usar o depurador e ver que tipo de configuração temos quando o UIKit chama o animator para realizar seu trabalho. Inicialmente temos a visualização de transição, apresentando o controlador que está na tela e o controlador modal que ainda não está na hierarquia de visualização.

(lldb) po source
<MYInitialNavigationController: 0x10a650620>
(lldb) po destination
<MYOtherNavigationController: 0x10a664580>

Ok, parece legítimo, mas espere um minuto, mais uma coisa …

(lldb) po source.view
<UILayoutContainerView: 0x10a733430; frame = (0 0; 320 568); autoresize = W+H; gestureRecognizers = <NSArray: 0x10a74c350>; layer = <CALayer: 0x10a733510>>

(lldb) po source.view.superview
<UITransitionView: 0x10a937560; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = H; layer = <CALayer: 0x10a938e30>>

(lldb) po [transitionContext containerView]
<UITransitionView: 0x10a937560; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = H; layer = <CALayer: 0x10a938e30>>

(lldb) po [source.view subviews]
<__NSArrayM 0x10a7456e0>(
<UINavigationTransitionView: 0x10a744ef0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x10a744ce0>>,
<UINavigationBar: 0x10a655a80; frame = (0 -44; 320 44); hidden = YES; opaque = NO; autoresize = W; gestureRecognizers = <NSArray: 0x10a80eee0>; layer = <CALayer: 0x10a64f290>>
)

Ótimo, então o UIKit simplesmente sequestrou meu controlador de visualização, envolveu sua visualização em alguns UILayoutContainerViewe fingiu ser eu. Deixe estar então.

É assim que a hierarquia de visualização fica depois de apresentarmos um controlador de visualização com transição modal personalizada:

Cenário

Diferença importante entre animações de apresentação e dispensa

  1. Quando apresentamos um controlador de visualização modal, o controlador de origem já está definido e tem sua supervisão, mas o controlador de destino ainda não está. Portanto, é nosso trabalho colocá-lo em fornecidos containerView. Podemos fazer isso imediatamente ou quando terminarmos com as animações.
  2. Quando descartamos um controlador de visão modal, ambos os controladores já estão na hierarquia de visão e são colocados dentro containerViewe não temos que gerenciar a hierarquia de visão.

Ao contrário das transições modais padrão, onde o controlador de exibição de apresentação é removido após a animação, ambos os controladores de exibição persistem na hierarquia em seus próprios contêineres. Esse é um dos problemas com eventos de aparência ausentes ( & co) para apresentar o controlador. Para corrigir esse problema, iniciei o procedimento que chamará métodos de aparência apropriados no controlador de exibição. Primeiro pensei que estava errado, mas parece funcionar.viewWillAppear:beginAppearanceTransition:animated:

Chega de teoria

Vamos fazer algumas animações finalmente. Como você pode ver no trecho de código abaixo, é muito trivial, ele se resume a uma animação simples entre duas visualizações. A única coisa que você tem que cuidar é ligar no final da transição.completeTransition:

- (void)animatePresentation:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* source = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController* destination = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView* container = transitionContext.containerView;

// Take destination view snapshot
UIView* destinationSS = [destination.view snapshotViewAfterScreenUpdates:YES]; // YES because the view hasn't been rendered yet.

// Add snapshot view
[container addSubview:destinationSS];

// Move destination snapshot back in Z plane
CATransform3D perspectiveTransform = CATransform3DIdentity;
perspectiveTransform
.m34 = 1.0 / -1000.0;
perspectiveTransform
= CATransform3DTranslate(perspectiveTransform, 0, 0, -100);
destinationSS
.layer.transform = perspectiveTransform;

// Start appearance transition for source controller
// Because UIKit does not remove views from hierarchy when transition finished
[source beginAppearanceTransition:NO animated:YES];

[UIView animateKeyframesWithDuration:0.5 delay:0.0 options:UIViewKeyframeAnimationOptionCalculationModeCubic animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0 animations:^{
CGRect sourceRect = source.view.frame;
sourceRect
.origin.y = CGRectGetHeight([[UIScreen mainScreen] bounds]);
source
.view.frame = sourceRect;
}];
[UIView addKeyframeWithRelativeStartTime:0.2 relativeDuration:0.8 animations:^{
destinationSS
.layer.transform = CATransform3DIdentity;
}];
} completion:^(BOOL finished) {
// Remove destination snapshot
[destinationSS removeFromSuperview];

// Add destination controller to view
[container addSubview:destination.view];

// Finish transition
[transitionContext completeTransition:finished];

// End appearance transition for source controller
[source endAppearanceTransition];
}];
}

Lembre-se de que você não deve mover as visualizações depois de chamar .completeTransition:

Ao animar entre os controladores de navegação, prefiro tirar um instantâneo do controlador invisível porque, nesse caso, você obtém uma barra de navegação alta de 64px adequada no instantâneo. Caso contrário, a animação do controlador de navegação leva diretamente a uma barra de 44px durante a animação e alguma falha quando encaixa na barra de status no final da animação. Outra boa razão é provavelmente o desempenho, mas não tenho números.

A animação de dispensa está praticamente fazendo o mesmo, mas ao contrário, então não a estou postando aqui.

Cenário

Desenrolar

O desenrolamento surpreendentemente não funciona fora da caixa, então você tem que chamar o dismissViewControllerAnimatedcontrolador apresentado. Eu faço isso na ação de desenrolar. A implementação se parece com a seguinte, mas você pode fazer um trabalho melhor e pelo menos verificar o identificador de segue:

- (IBAction)unwindToRootViewController:(UIStoryboardSegue*)unwindSegue {
[unwindSegue.sourceViewController dismissViewControllerAnimated:YES completion:nil];
}

Github, por favor

Você pode encontrar o código-fonte completo em https://github.com/pronebird/CustomModalTransition . Há um aplicativo de amostra bacana disponível para que você possa executá-lo e ver se as coisas funcionam para você.

Boa sorte!