Animer un changement de contrôleur de vue sans utiliser la pile de contrôleurs de navigation, les sous-vues ou les contrôleurs modaux?

142

Les NavigationControllers ont des piles ViewController à gérer et des transitions d'animation limitées.

L'ajout d'un contrôleur de vue en tant que sous-vue à un contrôleur de vue existant nécessite de transmettre des événements au contrôleur de sous-vue, ce qui est difficile à gérer, chargé de petits désagréments et en général ressemble à un mauvais hack lors de la mise en œuvre (Apple recommande également de ne pas Ce faisant).

La présentation d'un contrôleur de vue modale place à nouveau un contrôleur de vue au-dessus d'un autre, et bien qu'il n'ait pas les problèmes de passage d'événement décrits ci-dessus, il n'échange pas vraiment le contrôleur de vue, il l'empile.

Les storyboards sont limités à iOS 5 et sont presque idéaux, mais ne peuvent pas être utilisés dans tous les projets.

Quelqu'un peut-il présenter un EXEMPLE DE CODE SOLIDE sur un moyen de changer les contrôleurs de vue sans les limitations ci-dessus et permet des transitions animées entre eux?

Un exemple proche, mais pas d'animation: Comment utiliser plusieurs contrôleurs de vue personnalisés iOS sans contrôleur de navigation

Modifier: l'utilisation du contrôleur de navigation est correcte, mais il doit y avoir des styles de transition animés (pas simplement les effets de diapositive), le contrôleur de vue affiché doit être complètement permuté (pas empilé). Si le deuxième contrôleur de vue doit supprimer un autre contrôleur de vue de la pile, il n'est pas suffisamment encapsulé.

Edit 2: iOS 4 devrait être le système d'exploitation de base pour cette question, j'aurais dû clarifier cela en mentionnant les storyboards (ci-dessus).

TigerCoding
la source
1
Vous pouvez effectuer des transitions d'animation personnalisées avec un contrôleur de navigation. Si cela est acceptable, veuillez supprimer cette contrainte de votre question et je publierai un exemple de code.
Richard Brightwell
@Richard s'il évite les tracas de la gestion de la pile et accepte différents styles de transition animés entre les contrôleurs de vue, l'utilisation du contrôleur de navigation est bien!
TigerCoding
OK bien. Je me suis impatienté et j'ai posté le code. Essaie. Travaille pour moi.
Richard Brightwell
@RichardBrightwell vous avez dit ici qu'on pouvait faire des transitions d'animation personnalisées entre les contrôleurs de vue en utilisant un contrôleur de navigation ... comment? Pouvez-vous poster un exemple? Merci.
Canard

Réponses:

108

EDIT: Nouvelle réponse qui fonctionne dans n'importe quelle orientation. La réponse d'origine ne fonctionne que lorsque l'interface est en orientation portrait. Il s'agit d'animations de transition de vue b / c qui remplacent une vue avec une vue différente doivent se produire avec des vues au moins un niveau en dessous de la première vue ajoutée à la fenêtre (par exemplewindow.rootViewController.view.anotherView).

J'ai implémenté une classe de conteneur simple que j'ai appelée TransitionController. Vous pouvez le trouver sur https://gist.github.com/1394947 .

En passant, je préfère l'implémentation dans une classe distincte b / c c'est plus facile à réutiliser. Si vous ne le souhaitez pas, vous pouvez simplement implémenter la même logique directement dans votre délégué d'application, éliminant ainsi le besoin de la TransitionControllerclasse. La logique dont vous auriez besoin serait cependant la même.

Utilisez-le comme suit:

Dans votre délégué d'application

// 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;
}

Pour passer à un nouveau contrôleur de vue à partir de n'importe quel contrôleur de vue

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

EDIT: Réponse originale ci-dessous - ne fonctionne que pour l'orientation du portrait

J'ai fait les hypothèses suivantes pour cet exemple:

  1. Vous avez un contrôleur de vue affecté en tant que rootViewControllerde votre fenêtre

  2. Lorsque vous basculez vers une nouvelle vue, vous souhaitez remplacer le viewController actuel par le viewController propriétaire de la nouvelle vue. A tout moment, seul le viewController actuel est actif (par exemple alloué).

Le code peut être facilement modifié pour fonctionner différemment, le point clé est la transition animée et le contrôleur de vue unique. Assurez-vous de ne conserver aucun contrôleur de vue en dehors de son attribution window.rootViewController.

Code pour animer la transition dans le délégué d'application

- (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;
                    }];
}

Exemple d'utilisation dans un contrôleur de vue

- (IBAction)flipToNextView
{
    AnotherViewController *anotherVC = [[AnotherVC alloc] init...];
    MyAppDelegate *appDelegate = (MyAppDelegate *)[UIApplication sharedApplication].delegate;
    [appDelegate transitionToViewController:anotherVC
                             withTransition:UIViewAnimationOptionTransitionFlipFromRight];
}
XJones
la source
1
Oui très bien! Non seulement cela fait l'affaire, mais c'est un exemple de code très simple et propre. Merci beaucoup!
TigerCoding
Cela ne vous cause-t-il pas des problèmes de paysage? Aussi: cela déclenche-t-il les méthodes willAppear et didAppear des viewControllers?
Felix Lamouroux
1
Je sais que les appels apparaissent parce que je les ai enregistrés. Je ne vois pas non plus pourquoi cela affecterait les changements d'orientation. Pouvez-vous expliquer pourquoi vous pensez que ce serait le cas?
TigerCoding
1
@XJones excellente solution, fonctionne plutôt bien dans mon application iPad, sauf pour un problème auquel je suis confronté, après la première initialisation transVC = [TransitionController ... initWithViewController: aUINavCtrlObj]; window.rootVC = transVC; la vue dans aUINavCtrlObj est remplie à 20px du haut (c'est-à-dire que les 20 px du bas sont hors limites de l'écran (UIWindow) dans toutes les orientations), mais après que je fasse [transVC transitionToViewController: anotherVC] le remplissage est parti. J'ai essayé wantFullScreenLayout = NO dans le loadView de TransitionController, ce qu'il fait est qu'il ajoute une zone noire de 20 px juste sous statusBar.
Abduliam Rehmanius
1
@AbduliamRehmanius: J'ai eu le même problème. Je l'ai corrigé en changeant la ligne 25 de TransitionController.msur UIView *view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];, mais je ne l'ai utilisé que sur la toute dernière version d'iOS, alors testez attentivement.
Phil Calvin
67

Vous pouvez utiliser le nouveau système de confinement viewController d'Apple. Pour plus d'informations, regardez la vidéo de la session WWDC 2011 "Implémentation du UIViewControllerconfinement".

Nouveau dans iOS5, UIViewControllerContainment vous permet d'avoir un viewController parent et un certain nombre de viewControllers enfants qui y sont contenus. C'est ainsi que fonctionne UISplitViewController. En faisant cela, vous pouvez empiler des contrôleurs de vue dans un parent, mais pour votre application particulière, vous utilisez simplement le parent pour gérer la transition d'un viewController visible à un autre. C'est la façon approuvée par Apple de faire les choses et d'animer à partir d'une vue enfant. Le contrôleur est indolore. De plus, vous pouvez utiliser toutes les différentes UIViewAnimationOptiontransitions!

De plus, avec UIViewContainment, vous n'avez pas à vous soucier, sauf si vous le souhaitez, du désordre de la gestion des viewControllers enfants pendant les événements d'orientation. Vous pouvez simplement utiliser ce qui suit pour vous assurer que votre parentViewController transmet les événements de rotation aux viewControllers enfants.

- (BOOL)automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers{
    return YES;
}

Vous pouvez faire ce qui suit ou similaire dans la méthode viewDidLoad de vos parents pour configurer le premier childViewController:

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

puis, lorsque vous avez besoin de changer le viewController enfant, vous appelez quelque chose du type suivant dans le viewController parent:

-(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];
                             }];
 }

J'ai publié un exemple de projet complet ici: https://github.com/toolmanGitHub/stackedViewControllers . Cet autre projet montre comment utiliser UIViewControllerContainment sur certains types d'entrée viewController qui ne prennent pas tout l'écran. Bonne chance

timthetoolman
la source
1
Un bon exemple. Merci d'avoir pris le temps de poster le code. D'autre part, il est limité à iOS 5 uniquement. Comme mentionné dans la question: "[Les storyboards] sont limités à iOS 5, et sont presque idéaux, mais ne peuvent pas être utilisés dans tous les projets." Compte tenu d'un grand pourcentage (environ 40%?) De clients utilisent toujours iOS 4, l'objectif est de fournir quelque chose qui fonctionne sous iOS 4 et plus.
TigerCoding
Ne devriez-vous pas appeler [self.currentViewController willMoveToParentViewController:nil];avant la transition?
Felix Lamouroux
@FelixLam - selon la documentation sur le confinement UIViewController, vous appelez willMoveToParentViewController uniquement si vous substituez la méthode addChildViewController. Dans cet exemple, je l'appelle mais je ne remplace pas.
timthetoolman
2
Dans la démo de la WWDC, je semble me souvenir qu'ils l'ont appelé avant d'appeler le début de la transition, car la transition n'implique pas que l'actuelVC passera à zéro. Dans le cas d'un UITabBarController, la transition ne changerait aucun parent de vc. La suppression du parent appelle le didMoveToParentViewController: nil, mais il n'y a pas d'appel will ... IMHO
Félix Lamouroux
7

OK, je sais que la question dit sans utiliser de contrôleur de navigation, mais aucune raison de ne pas le faire. OP n'a pas répondu aux commentaires à temps pour que je m'endorme. Ne me rejetez pas. :)

Voici comment faire apparaître le contrôleur de vue actuel et basculer vers un nouveau contrôleur de vue à l'aide d'un contrôleur de navigation:

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
la source
Cette pile ne voit-elle pas les contrôleurs?
TigerCoding
1
Oui, car nous utilisons un contrôleur de navigation. Cependant, cela contourne la limitation du type de transitions que vous pouvez effectuer, ce qui, à mon avis, était au cœur de votre question.
Richard Brightwell
Fermer, mais l'un des gros problèmes est qu'il y a plusieurs contrôleurs de vue à gérer sur la pile. Je cherche un moyen de changer complètement les contrôleurs de vue. =)
TigerCoding
Ah. Je pourrais avoir quelque chose comme ça aussi ... donnez-moi une minute. Sinon, je supprimerai cette réponse.
Richard Brightwell
Ok, j'ai bricolé deux morceaux de code différents. Je pense que cela fera ce que vous voulez.
Richard Brightwell
3

Puisque je viens de rencontrer ce problème exact et que j'ai essayé des variantes de toutes les réponses préexistantes à un succès limité, je posterai comment je l'ai finalement résolu:

Comme décrit dans cet article sur les segues personnalisées , il est en fait très facile de créer des segues personnalisées. Ils sont également très faciles à connecter dans Interface Builder, ils gardent les relations dans IB visibles et ils ne nécessitent pas beaucoup de support de la part des contrôleurs de vue source / destination de la segue.

Le post lié ci-dessus fournit un code iOS 4 pour remplacer le contrôleur de vue de dessus actuel sur la pile navigationController par un nouveau à l'aide d'une animation de diapositive de haut.

Dans mon cas, je voulais qu'un remplacement similaire se produise, mais avec une FlipFromLefttransition. Je n'avais également besoin de support que pour iOS 5+. Code:

Depuis 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

Maintenant, contrôlez tout en faisant glisser pour configurer tout autre type de segue, puis faites-en un segue personnalisé et saisissez le nom de la classe de segue personnalisée, et voilà!

Ryan Artecona
la source
Y a-t-il un moyen d'y parvenir par programme sans storyboard?
zakdances
Autant que je sache, pour utiliser un segue, vous devez le définir et lui donner un identifiant dans un storyboard. Vous pouvez invoquer une séquence dans le code avec la –performSegueWithIdentifier:sender:méthode UIViewController .
Ryan Artecona
1
Vous ne devez jamais appeler viewDidXXX et viewWillXXX directement.
Ben Sinclair
2

J'ai lutté avec celui-ci pendant longtemps, et l'un de mes problèmes est répertorié ici , je ne sais pas si vous avez eu ce problème. Mais voici ce que je recommanderais s'il doit fonctionner avec iOS 4.

Tout d'abord, créez une nouvelle NavigationControllerclasse. C'est là que nous ferons tout le sale boulot - les autres classes pourront appeler "proprement" des méthodes d'instance comme pushViewController:et autres. Dans votre .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;

Le tableau de contrôleurs de vue enfant servira de magasin pour tous les contrôleurs de vue de notre pile. Nous transférerions automatiquement tout le code de rotation et de redimensionnement de la NavigationControllervue de vers le currentController.

Maintenant, dans notre implémentation:

- (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];
}

Vous pouvez maintenant implémenter votre propre coutume pushViewController:, popViewControlleret autres, en utilisant ces appels de méthode.

Bonne chance et j'espère que cela vous aidera!

aopsfan
la source
Encore une fois, nous devons recourir à l'ajout de contrôleurs de vue en tant que sous-vues des contrôleurs de vue existants. Certes, c'est ce que fait le contrôleur de navigation existant, mais cela signifie que nous devons essentiellement le réécrire et toutes ses méthodes. En réalité, nous devrions éviter d'avoir à distribuer viewWillAppear et des méthodes similaires. Je commence à penser qu'il n'y a pas de moyen propre de faire cela. Cependant, je vous remercie d'avoir pris le temps et les efforts!
TigerCoding
Je pense qu'avec ce système d'ajout et de suppression de contrôleurs de vue au besoin, cette solution vous évite d'avoir à transmettre ces méthodes.
aopsfan
Êtes-vous sûr? J'avais l'habitude d'échanger des contrôleurs de vue de la même manière auparavant et je devais toujours transférer des messages. Pouvez-vous confirmer le contraire?
TigerCoding
Non, je ne suis pas sûr, mais je suppose que, tant que vous supprimez les vues des contrôleurs de vue lorsqu'elles disparaissent, et que vous les ajoutez lorsqu'elles apparaissent, cela devrait se déclencher automatiquement viewWillAppear, viewDidAppearetc.
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];

Essayez ce code.


Ce code donne la transition d'un contrôleur de vue vers un autre contrôleur de vue ayant un contrôleur de navigation.

Deepak Kumar Sahu
la source