Animar a mudança de controladores de exibição sem usar pilha de controladores de navegação, sub-visualizações ou controladores modais?

142

Os NavigationControllers têm pilhas de ViewController para gerenciar e transições de animação limitadas.

A adição de um controlador de visualização como uma subvisão a um controlador de visualização existente requer a passagem de eventos para o controlador de subvisão, o que é uma tarefa difícil de gerenciar, carregada com poucos aborrecimentos e, em geral, parece um hack ruim na implementação (a Apple também recomenda Fazendo isso).

A apresentação de um controlador de exibição modal novamente coloca um controlador de exibição em cima de outro e, embora não tenha os problemas de passagem de eventos descritos acima, ele realmente não "troca" o controlador de exibição, ele o empilha.

Os storyboards são limitados ao iOS 5 e são quase ideais, mas não podem ser usados ​​em todos os projetos.

Alguém pode apresentar um EXEMPLO DE CÓDIGO SÓLIDO para alterar os controladores de exibição sem as limitações acima e permitir transições animadas entre eles?

Um exemplo próximo, mas sem animação: Como usar vários controladores de exibição personalizada do iOS sem um controlador de navegação

Edit: O uso do Nav Nav Controller é bom, mas é necessário que haja estilos de transição animados (não apenas os efeitos de slide); o controlador de exibição mostrado precisa ser trocado completamente (não empilhado). Se o segundo controlador de exibição precisar remover outro controlador de exibição da pilha, ele não será encapsulado o suficiente.

Editar 2: o iOS 4 deve ser o sistema operacional base para esta pergunta, eu deveria ter esclarecido isso ao mencionar os storyboards (acima).

TigerCoding
fonte
1
Você pode fazer transições de animação personalizadas com um controlador de navegação. Se isso for aceitável, remova essa restrição da sua pergunta e postarei um exemplo de código.
Richard Brightwell
@ Richard, se ignorar o incômodo de gerenciar a pilha e acomodar diferentes estilos de transição animados entre os controladores de exibição, o uso do controlador de navegação será bom!
TigerCoding
Tudo bem. Fiquei impaciente e postei o código. De uma chance. Funciona para mim.
precisa
@ RichardBrightwell, você disse aqui que é possível fazer transições de animação personalizadas entre os controladores de exibição usando um controlador de navegação ... como? Você pode postar um exemplo? obrigado.
Duck

Respostas:

108

EDIT: Nova resposta que funciona em qualquer orientação. A resposta original funciona apenas quando a interface está na orientação retrato. São animações de transição de vista b / c que substituem uma vista por uma vista diferente, devem ocorrer com vistas pelo menos um nível abaixo da primeira vista adicionada à janela (por exemplowindow.rootViewController.view.anotherView).

Eu implementei uma classe simples de contêiner que chamei TransitionController. Você pode encontrá-lo em https://gist.github.com/1394947 .

Como um aparte, prefiro a implementação em uma classe separada porque é mais fácil reutilizar. Se você não quiser isso, basta implementar a mesma lógica diretamente no delegado do aplicativo, eliminando a necessidade da TransitionControllerclasse. A lógica que você precisaria seria a mesma, no entanto.

Use-o da seguinte maneira:

No delegado do seu aplicativo

// add a property for the TransitionController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    MyViewController *vc = [[MyViewContoller alloc] init...];
    self.transitionController = [[TransitionController alloc] initWithViewController:vc];
    self.window.rootViewController = self.transitionController;
    [self.window makeKeyAndVisible];
    return YES;
}

Para fazer a transição para um novo controlador de exibição de qualquer controlador de exibição

- (IBAction)flipToView
{
    anotherViewController *vc = [[AnotherViewController alloc] init...];
    MyAppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
    [appDelegate.transitionController transitionToViewController:vc withOptions:UIViewAnimationOptionTransitionFlipFromRight];
}

EDIT: resposta original abaixo - só funciona para orientação portait

Fiz as seguintes suposições para este exemplo:

  1. Você tem um controlador de exibição atribuído como rootViewControllerda sua janela

  2. Quando você alterna para uma nova visualização, deseja substituir o viewController atual pelo viewController que possui a nova visualização. A qualquer momento, apenas o viewController atual está ativo (por exemplo, alocado).

O código pode ser facilmente modificado para funcionar de maneira diferente, o ponto principal é a transição animada e o controlador de visualização única. Certifique-se de não manter um controlador de exibição em nenhum lugar fora da atribuição a ele window.rootViewController.

Código para animar a transição no delegado do aplicativo

- (void)transitionToViewController:(UIViewController *)viewController
                    withTransition:(UIViewAnimationOptions)transition
{
    [UIView transitionFromView:self.window.rootViewController.view
                        toView:viewController.view
                      duration:0.65f
                       options:transition
                    completion:^(BOOL finished){
                        self.window.rootViewController = viewController;
                    }];
}

Exemplo de uso em um controlador de exibição

- (IBAction)flipToNextView
{
    AnotherViewController *anotherVC = [[AnotherVC alloc] init...];
    MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate;
    [appDelegate transitionToViewController:anotherVC
                             withTransition:UIViewAnimationOptionTransitionFlipFromRight];
}
XJones
fonte
1
Sim, muito bom! Não apenas funciona, mas é um exemplo de código muito simples e limpo. Muito Obrigado!
TigerCoding
Ele não causa problemas na paisagem para você? Além disso: isso aciona os métodos willAppear e didAppear dos viewControllers?
Felix Lamouroux
1
Eu sei que as chamadas de aparência estão sendo feitas porque eu as registrei. Também não vejo por que isso afetaria as mudanças de orientação. Você pode explicar por que você acha que seria?
TigerCoding
1
@XJones ótima solução, funciona muito bem no meu aplicativo para iPad, exceto por um problema que estou enfrentando, após a primeira inicialização transVC = [TransitionController ... initWithViewController: aUINavCtrlObj]; window.rootVC = transVC; a visualização no aUINavCtrlObj é preenchida 20px a partir do topo (ou seja, os 20 px inferiores ficam fora da tela (UIWindow) em todas as orientações), mas depois que eu faço [transVC transitToViewController: anotherVC], o preenchimento desaparece. Eu tentei wantFullScreenLayout = NO no loadView do TransitionController, o que ele faz é adicionar uma área preta de 20 px logo abaixo de statusBar.
Abduliam Rehmanius
1
@AbduliamRehmanius: Eu tive o mesmo problema. Corrigi-o alterando a linha 25 de TransitionController.mpara UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];, mas só o usei na versão mais recente do iOS, portanto teste cuidadosamente.
Phil Calvin
67

Você pode usar o novo sistema de contenção viewController da Apple. Para informações mais detalhadas, consulte o vídeo da sessão da WWDC 2011 "Implementing UIViewControllerConttainment".

Novo no iOS5, o UIViewControllerContainment permite que você tenha um viewController pai e um número de viewControllers filhos contidos nele. É assim que o UISplitViewController funciona. Fazendo isso, você pode empilhar os controladores de exibição em um pai, mas para seu aplicativo específico, você está apenas usando o pai para gerenciar a transição de um viewController visível para outro. Essa é a maneira aprovada pela Apple de fazer as coisas e a animação a partir de um único controle de exibição infantil é indolor. Além disso, você pode usar todas as várias UIViewAnimationOptiontransições diferentes !

Além disso, com o UIViewContainment, você não precisa se preocupar, a menos que queira, com a bagunça de gerenciar os viewControllers filhos durante eventos de orientação. Você pode simplesmente usar o seguinte para garantir que seu parentViewController encaminhe eventos de rotação para os viewControllers filhos.

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
    return YES;
}

Você pode fazer o seguinte ou semelhante no método viewDidLoad de seus pais para configurar o primeiro childViewController:

[self addChildViewController:self.currentViewController];
[self.view addSubview:self.currentViewController.view];
[self.currentViewController didMoveToParentViewController:self];
[self.currentViewController.swapViewControllerButton setTitle:@"Swap" forState:UIControlStateNormal];

quando você precisar alterar o viewController filho, chame algo semelhante ao seguinte no viewController pai:

-(void)swapViewControllers:(childViewController *)addChildViewController:aNewViewController{
     [self addChildViewController:aNewViewController];
     __weak __block ViewController *weakSelf=self;
     [self transitionFromViewController:self.currentViewController
                       toViewController:aNewViewController
                               duration:1.0
                                options:UIViewAnimationOptionTransitionCurlUp
                             animations:nil
                             completion:^(BOOL finished) {
                                   [aNewViewController didMoveToParentViewController:weakSelf];

                                   [weakSelf.currentViewController willMoveToParentViewController:nil];
                                   [weakSelf.currentViewController removeFromParentViewController];

                                   weakSelf.currentViewController=[aNewViewController autorelease];
                             }];
 }

Publiquei um projeto de exemplo completo aqui: https://github.com/toolmanGitHub/stackedViewControllers . Este outro projeto mostra como usar o UIViewControllerContainment em vários tipos de viewController de entrada que não ocupam a tela inteira. Boa sorte

timthetoolman
fonte
1
Um ótimo exemplo. Obrigado por reservar um tempo para publicar o código. Por outro lado, é limitado apenas ao iOS 5. Como mencionado na pergunta: "[Storyboards] são limitados ao iOS 5 e são quase ideais, mas não podem ser usados ​​em todos os projetos". Considerando que uma grande porcentagem (cerca de 40%?) Dos clientes ainda usa o iOS 4, o objetivo é fornecer algo que funcione no iOS 4 e superior.
TigerCoding
Você não deve ligar [self.currentViewController willMoveToParentViewController:nil];antes da transição?
Felix Lamouroux
@FelixLam - de acordo com os documentos sobre a contenção UIViewController, você chamará willMoveToParentViewController apenas se você substituir o método addChildViewController. Neste exemplo, estou chamando, mas não substituindo.
timthetoolman
2
Na demonstração da WWDC, lembro que eles o chamaram antes de iniciar a transição, porque a transição não implica que o CV atual se mova para zero. No caso de um UITabBarController, a transição não alteraria nenhum pai de vc. A remoção do pai chama o didMoveToParentViewController: nil, mas não há vontade ... chamada. IMHO
Felix Lamouroux
7

OK, eu sei que a pergunta diz sem usar um controlador de navegação, mas não há razão para não fazê-lo. A OP não estava respondendo aos comentários a tempo de eu ir dormir. Não vote em mim. :)

Veja como abrir o controlador de exibição atual e alternar para um novo controlador de exibição usando um controlador de navegação:

UINavigationController *myNavigationController = self.navigationController;
[[self retain] autorelease];

[myNavigationController popViewControllerAnimated:NO];

PreferencesViewController *controller = [[PreferencesViewController alloc] initWithNibName:nil bundle:nil];

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.65];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:myNavigationController.view cache:YES];
[myNavigationController pushViewController:controller animated:NO];
[UIView commitAnimations];

[controller release];
Richard Brightwell
fonte
Essa pilha não exibe controladores?
TigerCoding
1
Sim, porque estamos usando um controlador de navegação. No entanto, contorna a limitação de que tipo de transição você pode executar, o que eu pensei que era o cerne da sua pergunta.
precisa
Feche, mas um dos grandes problemas é que existem vários controladores de exibição para gerenciar na pilha. Estou procurando uma maneira de mudar completamente os controladores de exibição. =)
TigerCoding
Ah Eu posso ter algo assim também ... me dê um minuto. Caso contrário, vou excluir esta resposta.
precisa
Ok, juntei dois bits diferentes de código. Eu acho que isso fará o que você quiser.
Richard Brightwell
3

Como acabei de encontrar esse problema exato e tentei variações de todas as respostas pré-existentes para um sucesso limitado, postarei como resolvi o problema:

Conforme descrito nesta postagem sobre sequências personalizadas , é realmente muito fácil fazer sequências personalizadas. Eles também são super fáceis de conectar no Interface Builder, mantêm os relacionamentos no IB visíveis e não exigem muito suporte dos controladores de exibição de origem / destino do segue.

A postagem vinculada acima fornece o código do iOS 4 para substituir o atual controlador de vista superior na pilha navigationController por um novo usando uma animação slide-in-from-top.

No meu caso, eu queria uma substituição semelhante segue, mas com uma FlipFromLefttransição. Eu também só precisava de suporte para o iOS 5 ou superior. Código:

De RAFlipReplaceSegue.h:

#import <UIKit/UIKit.h>

@interface RAFlipReplaceSegue : UIStoryboardSegue
@end

De RAFlipReplaceSegue.m:

#import "RAFlipReplaceSegue.h"

@implementation RAFlipReplaceSegue

-(void) perform
{
    UIViewController *destVC = self.destinationViewController;
    UIViewController *sourceVC = self.sourceViewController;
    [destVC viewWillAppear:YES];

    destVC.view.frame = sourceVC.view.frame;

    [UIView transitionFromView:sourceVC.view
                        toView:destVC.view
                      duration:0.7
                       options:UIViewAnimationOptionTransitionFlipFromLeft
                    completion:^(BOOL finished)
                    {
                        [destVC viewDidAppear:YES];

                        UINavigationController *nav = sourceVC.navigationController;
                        [nav popViewControllerAnimated:NO];
                        [nav pushViewController:destVC animated:NO];
                    }
     ];
}

@end

Agora, arraste com a tecla Control pressionada para configurar qualquer outro tipo de segue, em seguida, torne-o personalizado segue e digite o nome da classe segue personalizada, e pronto!

Ryan Artecona
fonte
Existe uma maneira de fazer isso de forma programática sem storyboard?
precisa saber é
Tanto quanto eu sei, para usar um segue, você deve defini-lo e fornecer um identificador em um storyboard. Você pode chamar um segue no código com o –performSegueWithIdentifier:sender:método UIViewController .
Ryan Artecona
1
Você nunca deve chamar viewDidXXX e viewWillXXX diretamente.
Ben Sinclair
2

Lutei com esse problema por um longo tempo e um dos meus problemas está listado aqui , não tenho certeza se você teve esse problema. Mas aqui está o que eu recomendaria se ele deve funcionar com o iOS 4.

Primeiro, crie uma nova NavigationControllerclasse. É aqui que faremos todo o trabalho sujo - outras classes poderão "limpa" chamar métodos de instância como pushViewController:esses. No seu .h:

@interface NavigationController : UIViewController {
    NSMutableArray *childViewControllers;
    UIViewController *currentViewController;
}

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion;
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeChildViewController:(UIViewController *)childController;

A matriz de controladores de exibição filho servirá como um armazenamento para todos os controladores de exibição em nossa pilha. Encaminharíamos automaticamente todo o código de rotação e redimensionamento da NavigationControllervisualização para a currentController.

Agora, em nossa implementação:

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL))completion
{
    currentViewController = [toViewController retain];
    // Put any auto- and manual-resizing handling code here

    [UIView animateWithDuration:duration animations:animations completion:completion];

    [fromViewController.view removeFromSuperview];
}

- (void)addChildViewController:(UIViewController *)childController {
    [childViewControllers addObject:childController];
}

- (void)removeChildViewController:(UIViewController *)childController {
    [childViewControllers removeObject:childController];
}

Agora você pode implementar seu próprio costume pushViewController:, popViewControllere tal, usando essas chamadas de método.

Boa sorte, e espero que isso ajude!

aopsfan
fonte
Novamente, devemos recorrer à adição de controladores de exibição como sub-exibições de controladores de exibição existentes. É verdade que isso é o que o Navigation Controller existente faz, mas significa que basicamente precisamos reescrevê-lo e todos os seus métodos. Na realidade, devemos evitar distribuir o viewWillAppear e métodos semelhantes. Estou começando a pensar que não há uma maneira limpa de fazer isso. No entanto, agradeço por dedicar seu tempo e esforço!
TigerCoding
Penso que, com este sistema de adição e remoção de controladores de visualização, conforme necessário, esta solução impede que você precise encaminhar esses métodos.
aopsfan
Você tem certeza? Eu costumava trocar os controladores de exibição de maneira semelhante antes e sempre tinha que encaminhar mensagens. Você pode confirmar o contrário?
TigerCoding
Não, eu não tenho certeza, mas eu diria que, enquanto você remover vistas a vista dos controladores como eles desaparecem, e adicioná-los como eles aparecem, que deve automaticamente gatilho viewWillAppear, viewDidAppeare tal.
aopsfan
-1
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
UINavigationController *viewController = (UINavigationController *)[storyboard instantiateViewControllerWithIdentifier:@"storyBoardIdentifier"];
viewController.modalTransitionStyle = UIModalTransitionStylePartialCurl;
[self presentViewController:viewController animated:YES completion:nil];

Experimente este código.


Esse código fornece a transição de um controlador de exibição para outro controlador de exibição que possui um controlador de navegação.

Deepak Kumar Sahu
fonte