web-dev-qa-db-fra.com

Comment puis-je afficher une vue d'un UINavigationController et la remplacer par une autre au cours d'une opération?

J'ai une application pour laquelle je dois supprimer une vue de la pile d'un UINavigationController et la remplacer par une autre. La situation est que la première vue crée un élément modifiable, puis se remplace par un éditeur pour l'élément. Quand je fais la solution évidente dans la première vue:

MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

[self retain];
[self.navigationController popViewControllerAnimated: NO];
[self.navigationController pushViewController: mevc animated: YES];
[self release];

J'ai un comportement très étrange. Généralement, la vue de l'éditeur apparaît, mais si j'essaie d'utiliser le bouton de retour de la barre de navigation, des écrans supplémentaires, certains vierges et d'autres simplement foirés, sont affichés. Le titre devient aléatoire aussi. C'est comme si la pile de navigation était complètement arrosée.

Quelle serait une meilleure approche de ce problème?

Merci, Matt.

83
Matt Brandt

J'ai découvert que vous n'avez pas besoin de manipuler manuellement la propriété viewControllers. Fondamentalement, il y a 2 choses délicates à ce sujet.

  1. self.navigationController renverra nil si self n'est pas actuellement sur la pile du contrôleur de navigation. Enregistrez-le dans une variable locale avant de perdre son accès.
  2. Vous devez retain (et correctement release) self ou l'objet à qui appartient la méthode dans laquelle vous vous trouvez sera désalloué, ce qui causera son étrangeté.

Une fois que vous avez fait cette préparation, il suffit ensuite de faire apparaître et de pousser comme d'habitude. Ce code remplacera instantanément le contrôleur supérieur par un autre.

// locally store the navigation controller since
// self.navigationController will be nil once we are popped
UINavigationController *navController = self.navigationController;

// retain ourselves so that the controller will still exist once it's popped off
[[self retain] autorelease];

// Pop this controller and replace with another
[navController popViewControllerAnimated:NO];
[navController pushViewController:someViewController animated:NO];

Dans cette dernière ligne, si vous modifiez la variable animated en YES, le nouvel écran s'animera et le contrôleur que vous venez de décompresser s'animera. Semble jolie Nice!

137
Alex Wayne

L'approche suivante me semble plus agréable et fonctionne également bien avec ARC:

UIViewController *newVC = [[UIViewController alloc] init];
// Replace the current view controller
NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
[viewControllers removeLastObject];
[viewControllers addObject:newVC];
[[self navigationController] setViewControllers:viewControllers animated:YES];
56
Luke Rogers

Par expérience, vous allez devoir manipuler directement la propriété viewControllers de UINavigationController. Quelque chose comme ça devrait marcher:

MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

[[self retain] autorelease];
NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];
[controllers removeLastObject];
self.navigationController.viewControllers = controllers;
[self.navigationController pushViewController:mevc animated: YES];

Remarque: j'ai modifié l'option Conserver/Valider pour une retenue/Autorelease car elle est généralement plus robuste. Si une exception se produit entre la conservation/la libération, vous fuyez tout seul, mais autorelease s'en charge.

9
Lily Ballard

Après beaucoup d'efforts (et en peaufinant le code de Kevin), j'ai finalement compris comment procéder dans le contrôleur de vue extrait de la pile. Le problème que j'avais était que self.navigationController retournait nil après avoir supprimé le dernier objet du tableau de contrôleurs. Je pense que cela était dû à cette ligne dans la documentation de UIViewController sur la méthode d'instance navigationController "Ne renvoie un contrôleur de navigation que si le contrôleur de vue se trouve dans sa pile."

Je pense qu'une fois que le contrôleur de vue actuel est retiré de la pile, sa méthode navigationController retournera nil.

Voici le code ajusté qui fonctionne:

UINavigationController *navController = self.navigationController;
MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];
[controllers removeLastObject];
navController.viewControllers = controllers;
[navController pushViewController:mevc animated: YES];
7
Michael

Merci, c'était exactement ce dont j'avais besoin. Je mets également cela dans une animation pour obtenir la page courbée:

        MyEditViewController *mevc = [[MYEditViewController alloc] initWithGizmo: gizmo];

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

    [UIView beginAnimations:nil context:NULL]; [UIView setAnimationDuration: 0.7];
    [UIView setAnimationTransition:<#UIViewAnimationTransitionCurlDown#> forView:navController.view cache:NO];

    [navController popViewControllerAnimated:NO];
    [navController pushViewController:mevc animated:NO];

    [UIView commitAnimations];

0,6 durée est rapide, bon pour 3GS et plus récent, 0,8 est encore un peu trop rapide pour 3G ..

Johan

4
Johan

Si vous souhaitez afficher un autre contrôleur de vue par popToRootViewController, procédez comme suit:

         UIViewController *newVC = [[WelcomeScreenVC alloc] initWithNibName:@"WelcomeScreenVC" bundle:[NSBundle mainBundle]];
            NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[[self navigationController] viewControllers]];
            [viewControllers removeAllObjects];
            [viewControllers addObject:newVC];
            [[self navigationController] setViewControllers:viewControllers animated:NO];

Maintenant, toute votre pile précédente sera supprimée et une nouvelle pile sera créée avec votre rootViewController requis.

3
msmq

Ma façon préférée de le faire est avec une catégorie sur UINavigationController. Ce qui suit devrait fonctionner:

UINavigationController + Helpers.h #importation 

@interface UINavigationController (Helpers)

- (UIViewController*) replaceTopViewControllerWithViewController: (UIViewController*) controller;

@end

UINavigationController + Helpers.m
#import "UINavigationController + Helpers.h"

@implementation UINavigationController (Helpers)

- (UIViewController*) replaceTopViewControllerWithViewController: (UIViewController*) controller {
    UIViewController* topController = self.viewControllers.lastObject;
    [[topController retain] autorelease];
    UIViewController* poppedViewController = [self popViewControllerAnimated:NO];
    [self pushViewController:controller animated:NO];
    return poppedViewController;
}

@end

Ensuite, depuis votre contrôleur de vue, vous pouvez remplacer la vue de dessus par une nouvelle par ceci:

[self.navigationController replaceTopViewControllerWithViewController: newController];
1
bbrame

Cette méthode d'instance UINavigationController pourrait fonctionner ...

Affiche les contrôleurs de vue jusqu'à ce que le contrôleur de vue spécifié soit le contrôleur de vue de dessus, puis met à jour l'affichage.

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
1
diclophis
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
    for(int i=0;i<controllers.count;i++){
       [controllers removeLastObject];
    }
 self.navigationController.viewControllers = controllers;
1
Ravi

Voici une autre approche qui ne nécessite pas de jouer directement avec le tableau viewControllers. Vérifiez si le contrôleur a déjà été activé, le cas échéant, appuyez dessus.

TasksViewController *taskViewController = [[TasksViewController alloc] initWithNibName:nil bundle:nil];

if ([navigationController.viewControllers indexOfObject:taskViewController] == NSNotFound)
{
    [navigationController pushViewController:taskViewController animated:animated];
}
else
{
    [navigationController popToViewController:taskViewController animated:animated];
}
1
ezekielDFM

Je devais faire une chose similaire récemment et basé ma solution sur la réponse de Michaels. Dans mon cas, je devais supprimer deux contrôleurs de vue de la pile de navigation, puis ajouter un nouveau contrôleur de vue sur. Appel

[contrôleurs removeLastObject];
UINavigationController *navController = self.navigationController;

// retain ourselves so that the controller will still exist once it's popped off
[[self retain] autorelease];

searchViewController = [[SearchViewController alloc] init];    
NSMutableArray *controllers = [[self.navigationController.viewControllers mutableCopy] autorelease];

[controllers removeLastObject];
// In my case I want to go up two, then Push one..
[controllers removeLastObject];
navController.viewControllers = controllers;

NSLog(@"controllers: %@",controllers);
controllers = nil;

[navController pushViewController:searchViewController animated: NO];
 </ code>
1
mattlangtree

Pour IOS monotouch/xamarin:

dans la classe UISplitViewController;

UINavigationController mainNav = this._navController; 
//List<UIViewController> controllers = mainNav.ViewControllers.ToList();
mainNav.ViewControllers = new UIViewController[] { }; 
mainNav.PushViewController(detail, true);//to have the animation
0
Nabil.A

Pas exactement la réponse, mais pourrait être utile dans certains scénarios (le mien par exemple):

Si vous avez besoin d'afficher le contrôleur C et d'aller à B (hors pile) au lieu de A (le soufflet C ci-dessous), il est possible de pousser B avant C et d'avoir les 3 sur la pile. En gardant le bouton B invisible, et en choisissant si vous souhaitez uniquement afficher uniquement C ou C et B, vous pouvez obtenir le même effet.

problème initial A -> C (je veux faire apparaître C et montrer B, hors pile)

solution possible A -> B (push invisible) -> C (quand je saute C, je choisis de montrer B ou aussi de le sauter)

0
alasker

Vous pouvez vérifier avec le tableau de contrôleurs de vue de navigation que vous vous donnez tous les contrôleurs de vue que vous avez ajoutés dans la pile de navigation. En utilisant ce tableau, vous pouvez revenir à un contrôleur de vue spécifique.

0
jignesh

J'utilise cette solution pour garder l'animation.

[self.navigationController pushViewController:controller animated:YES];
NSMutableArray *newControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
[newControllers removeObject:newControllers[newControllers.count - 2]];
[self.navigationController setViewControllers:newControllers];
0
code4j

Alternativement

Vous pouvez utiliser category pour éviter que self.navigationController devienne nil après popViewControllerAnimated

il suffit de sauter et de pousser, c'est facile à comprendre, il n'est pas nécessaire d'accéder à viewControllers....

// UINavigationController+Helper.h
@interface UINavigationController (Helper)

- (UIViewController*) popThenPushViewController:(UIViewController *)viewController animated:(BOOL)animated;

@end


// UINavigationController+Helper.m
@implementation UINavigationController (Helper)

- (UIViewController*) popThenPushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    UIViewController *v =[self popViewControllerAnimated:NO];

    [self pushViewController:viewController animated:animated];

    return v;
}
@end

Dans votre ViewController

// #import "UINavigationController+Helper.h"
// invoke in your code
UIViewController *v= [[MyNewViewController alloc] init];

[self.navigationController popThenPushViewController:v animated:YES];

RELEASE_SAFELY(v);
0
payliu