J'ai récemment essayé de faire quelque chose de similaire. J'ai décidé que je n'aimais pas l'animation de glissement de UINavigationController, mais je ne voulais pas non plus faire les animations que UIView vous donne comme curl ou quelque chose comme ça. Je voulais faire un fondu enchaîné entre les vues lorsque je pousse ou que je saute.
Le problème réside dans le fait que la vue est littéralement en train de supprimer la vue ou d'en ajouter une par-dessus la vue actuelle, de sorte qu'un fondu ne fonctionne pas. La solution que j'ai trouvée consiste à prendre ma nouvelle vue et à l'ajouter en tant que sous-vue à la vue supérieure actuelle sur la pile du UIViewController. Je l'ajoute avec un alpha de 0, puis je fais un fondu enchaîné. Lorsque la séquence d'animation se termine, je pousse la vue sur la pile sans l'animer. Je retourne ensuite à l'ancienne vue supérieure et nettoie les éléments que j'ai modifiés.
C'est un peu plus compliqué que cela, car il faut ajuster les éléments de navigation pour que la transition soit correcte. De plus, si vous effectuez une rotation, vous devez ensuite ajuster la taille des cadres lorsque vous ajoutez les vues en tant que sous-vues afin qu'elles s'affichent correctement à l'écran. Voici une partie du code que j'ai utilisé. J'ai sous-classé le UINavigationController et surchargé les méthodes push et pop.
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
UIViewController *currentViewController = [self.viewControllers lastObject];
//if we don't have a current controller, we just do a normal push
if(currentViewController == nil)
{
[super pushViewController:viewController animated:animated];
return;
}
//if no animation was requested, we can skip the cross fade
if(!animation)
{
[super pushViewController:viewController animated:NO];
return;
}
//start the cross fade. This is a tricky thing. We basically add the new view
//as a subview of the current view, and do a cross fade through alpha values.
//then we push the new view on the stack without animating it, so it seemlessly is there.
//Finally we remove the new view that was added as a subview to the current view.
viewController.view.alpha = 0.0;
//we need to hold onto this value, we'll be releasing it later
NSString *title = [currentViewController.title retain];
//add the view as a subview of the current view
[currentViewController.view addSubview:viewController.view];
[currentViewController.view bringSubviewToFront:viewController.view];
UIBarButtonItem *rButtonItem = currentViewController.navigationItem.rightBarButtonItem;
UIBarButtonItem *lButtonItem = currentViewController.navigationItem.leftBarButtonItem;
NSArray *array = nil;
//if we have a right bar button, we need to add it to the array, if not, we will crash when we try and assign it
//so leave it out of the array we are creating to pass as the context. I always have a left bar button, so I'm not checking to see if it is nil. Its a little sloppy, but you may want to be checking for the left BarButtonItem as well.
if(rButtonItem != nil)
array = [[NSArray alloc] initWithObjects:currentViewController,viewController,title,lButtonItem,rButtonItem,nil];
else {
array = [[NSArray alloc] initWithObjects:currentViewController,viewController,title,lButtonItem,nil];
}
//remove the right bar button for our transition
[currentViewController.navigationItem setRightBarButtonItem:nil animated:YES];
//remove the left bar button and create a backbarbutton looking item
//[currentViewController.navigationItem setLeftBarButtonItem:nil animated:NO];
//set the back button
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:title style:kButtonStyle target:self action:@selector(goBack)];
[currentViewController.navigationItem setLeftBarButtonItem:backButton animated:YES];
[viewController.navigationItem setLeftBarButtonItem:backButton animated:NO];
[backButton release];
[currentViewController setTitle:viewController.title];
[UIView beginAnimations:@"push view" context:array];
[UIView setAnimationDidStopSelector:@selector(animationForCrossFadePushDidStop:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.80];
[viewController.view setAlpha: 1.0];
[UIView commitAnimations];
}
-(void)animationForCrossFadePushDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
UIViewController *c = [(NSArray*)context objectAtIndex:0];
UIViewController *n = [(NSArray*)context objectAtIndex:1];
NSString *title = [(NSArray *)context objectAtIndex:2];
UIBarButtonItem *l = [(NSArray *)context objectAtIndex:3];
UIBarButtonItem *r = nil;
//not all views have a right bar button, if we look for it and it isn't in the context,
//we'll crash out and not complete the method, but the program won't crash.
//So, we need to check if it is there and skip it if it isn't.
if([(NSArray *)context count] == 5)
r = [(NSArray *)context objectAtIndex:4];
//Take the new view away from being a subview of the current view so when we go back to it
//it won't be there anymore.
[[[c.view subviews] lastObject] removeFromSuperview];
[c setTitle:title];
[title release];
//set the search button
[c.navigationItem setLeftBarButtonItem:l animated:NO];
//set the next button
if(r != nil)
[c.navigationItem setRightBarButtonItem:r animated:NO];
[super pushViewController:n animated:NO];
}
Comme je le mentionne dans le code, j'ai toujours un bouton de la barre de gauche, donc je ne vérifie pas s'il est nul avant de le mettre dans le tableau que je passe comme contexte pour le délégué d'animation. Si vous faites cela, vous voudrez peut-être faire cette vérification.
Le problème que j'ai rencontré est que si vous vous plantez dans la méthode du délégué, le programme ne se plante pas. Il empêche simplement le délégué de se terminer, mais vous n'obtenez aucune sorte d'avertissement.
Donc, comme je faisais mon nettoyage dans cette routine déléguée, cela provoquait un comportement visuel bizarre car le nettoyage n'était pas terminé.
Le bouton de retour que je crée appelle une méthode "goBack", et cette méthode appelle simplement la routine pop.
-(void)goBack
{
[self popViewControllerAnimated:YES];
}
Aussi, voici ma routine pop.
-(UIViewController *)popViewControllerAnimated:(BOOL)animated
{
//get the count for the number of viewControllers on the stack
int viewCount = [[self viewControllers] count];
//get the top view controller on the stack
UIViewController *topViewController = [self.viewControllers objectAtIndex:viewCount - 1];
//get the next viewController after the top one (this will be the new top one)
UIViewController *newTopViewController = [self.viewControllers objectAtIndex:viewCount - 2];
//if no animation was requested, we can skip the cross fade
if(!animated)
{
[super popViewControllerAnimated:NO];
return topViewController;
}
//start of the cross fade pop. A bit tricky. We need to add the new top controller
//as a subview of the curent view controler with an alpha of 0. We then do a cross fade.
//After that we pop the view controller off the stack without animating it.
//Then the cleanup happens: if the view that was popped is not released, then we
//need to remove the subview we added and change some titles back.
newTopViewController.view.alpha = 0.0;
[topViewController.view addSubview:newTopViewController.view];
[topViewController.view bringSubviewToFront:newTopViewController.view];
NSString *title = [topViewController.title retain];
UIBarButtonItem *lButtonItem = topViewController.navigationItem.leftBarButtonItem;
UIBarButtonItem *rButtonItem = topViewController.navigationItem.rightBarButtonItem;
//set the new buttons on top of the current controller from the new top controller
if(newTopViewController.navigationItem.leftBarButtonItem != nil)
{
[topViewController.navigationItem setLeftBarButtonItem:newTopViewController.navigationItem.leftBarButtonItem animated:YES];
}
if(newTopViewController.navigationItem.rightBarButtonItem != nil)
{
[topViewController.navigationItem setRightBarButtonItem:newTopViewController.navigationItem.rightBarButtonItem animated:YES];
}
[topViewController setTitle:newTopViewController.title];
//[topViewController.navigationItem.leftBarButtonItem setTitle:newTopViewController.navigationItem.leftBarButtonItem.title];
NSArray *array = nil;
if(rButtonItem != nil)
array = [[NSArray alloc] initWithObjects:topViewController,title,lButtonItem,rButtonItem,nil];
else {
array = [[NSArray alloc] initWithObjects:topViewController,title,lButtonItem,nil];
}
[UIView beginAnimations:@"pop view" context:array];
[UIView setAnimationDidStopSelector:@selector(animationForCrossFadePopDidStop:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.80];
[newTopViewController.view setAlpha: 1.0];
[UIView commitAnimations];
return topViewController;
}
-(void)animationForCrossFadePopDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{
UIViewController *c = [(NSArray *)context objectAtIndex:0];
//UIViewController *n = [(NSArray *)context objectAtIndex:1];
NSString *title = [(NSArray *)context objectAtIndex:1];
UIBarButtonItem *l = [(NSArray *)context objectAtIndex:2];
UIBarButtonItem *r = nil;
//Not all views have a right bar button. If we look for one that isn't there
// we'll crash out and not complete this method, but the program will continue.
//So we need to check if it is therea nd skip it if it isn't.
if([(NSArray *)context count] == 4)
r = [(NSArray *)context objectAtIndex:3];
//pop the current view from the stack without animation
[super popViewControllerAnimated:NO];
//if what was the current veiw controller is not nil, then lets correct the changes
//we made to it.
if(c != nil)
{
//remove the subview we added for the transition
[[c.view.subviews lastObject] removeFromSuperview];
//reset the title we changed
c.title = title;
[title release];
//replace the left bar button that we changed
[c.navigationItem setLeftBarButtonItem:l animated:NO];
//if we were passed a right bar button item, replace that one as well
if(r != nil)
[c.navigationItem setRightBarButtonItem:r animated:NO];
else {
[c.navigationItem setRightBarButtonItem:nil animated:NO];
}
}
}
C'est à peu près tout. Vous aurez besoin de code supplémentaire si vous voulez implémenter des rotations. Vous devrez définir la taille d'image de vos vues que vous ajoutez en tant que sous-vues avant de les afficher, sinon vous rencontrerez des problèmes : l'orientation est paysage, mais la dernière fois que vous avez vu la vue précédente, elle était portrait. Donc, vous l'ajoutez en tant que sous-vue et vous l'introduisez en fondu mais elle apparaît en portrait, puis lorsque nous l'ouvrons sans animation, la même vue, mais celle qui est dans la pile, est maintenant en paysage. Tout cela a l'air un peu bizarre. L'implémentation de la rotation est un peu différente d'une personne à l'autre, donc je n'ai pas inclus mon code pour cela ici.
J'espère que ça aidera certaines personnes. J'ai cherché partout quelque chose comme ça et je n'ai rien trouvé. Je ne pense pas que ce soit la réponse parfaite, mais cela fonctionne très bien pour moi à ce stade.
26 votes
À partir d'iOS 7, il existe une API officielle pour cela ; voir UINavigationControllerDelegate La prise en charge des animations de transition personnalisées de l'entreprise. Il y a également un Vidéo de la WWDC 2013 à ce sujet.
0 votes
J'ai ajouté une réponse (ci-dessous) pour faire cela en Swift. Je suis tombé sur cette question concernant les implémentations Swift et j'ai pensé que je devais y participer avec mon implémentation ultérieure.
1 votes
Pour un très bon tutoriel avec l'API officielle (iOS 7+), voir : bradbambara.wordpress.com/2014/04/11/
1 votes
@JesseRusak a mis à jour le lien vers la vidéo de la WWDC 2013 : developer.apple.com/videos/play/wwdc2013-218
0 votes
Pour 2018 ! Pour ce très ancien AQ, on utilise aujourd'hui UIViewControllerAnimatedTransitioning . J'ai mis une réponse en bas de page. Bonne année !
1 votes
J'ai changé ma réponse acceptée, les gars et les filles. J'espère que cela vous aidera ! GLHF