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

84

J'ai une application dans 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. Habituellement, la vue de l'éditeur apparaîtra, mais si j'essaie d'utiliser le bouton de retour de la barre de navigation, j'obtiens des écrans supplémentaires, certains vides et d'autres tout simplement foutus. Le titre devient également aléatoire. C'est comme si la pile de navigation était complètement arrosée.

Quelle serait la meilleure approche à ce problème?

Merci, Matt

Matt Brandt
la source

Réponses:

137

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

  1. self.navigationControllerretournera nilsi selfn'est pas actuellement sur la pile du contrôleur de navigation. Enregistrez-le donc dans une variable locale avant de perdre l'accès.
  2. Vous devez retain(et correctement release) selfou l'objet qui possède la méthode dans laquelle vous vous trouvez sera désalloué, ce qui provoquera l'étrangeté.

Une fois que vous avez fait cette préparation, alors pop et poussez 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 remplacez le animatedpar YES, le nouvel écran s'animera et le contrôleur que vous venez de faire apparaître s'animera. Ça a l'air plutôt sympa!

Alex Wayne
la source
brillant! bien meilleure solution
emmby
Impressionnant. Bien que je n'ai pas eu besoin d'appeler [[self retention] autorelease], cela fonctionne toujours très bien.
iamj4de
4
Peut-être un ajout évident, mais vous pouvez ensuite mettre le code ci-dessus dans un bloc d'animation pour animer la transition: [UIView beginAnimations: @ "View Flip" context: nil]; [UIView setAnimationDuration: 0.80]; [UIView setAnimationCurve: UIViewAnimationCurveEaseInOut]; [UIView setAnimationTransition: UIViewAnimationTransitionFlipFromRight forView: navController.view cache: NO]; [navController pushViewController: newController animé: OUI]; [UIView commitAnimations];
Martin
11
Fonctionne très bien avec ARC simplement en supprimant la ligne de rétention / libération automatique.
Ian Terrell
2
@TomerPeled Oui, cette réponse a presque 5 ans ... Je pense que c'était le cas dans comme iOS 3. Les API ont suffisamment changé pour que je ne sois plus sûr que ce soit la meilleure réponse.
Alex Wayne
56

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];
Luke Rogers
la source
1
@LukeRogers, cela provoque l'avertissement suivant pour moi: Fin d'une transition de navigation dans un état inattendu. L'arborescence de la sous-vue de la barre de navigation peut être corrompue. Un moyen de le supprimer?
zaitsman
En utilisant cette solution, vous écrasez le popover. Et pour afficher dans le DetailView, votre code doit lire:if(indexPath.row == 0){UIViewController *newVC = [[UIViewController alloc] init];newVC = [self.storyboard instantiateViewControllerWithIdentifier:@"Item1VC"]; NSMutableArray *viewControllers = [NSMutableArray arrayWithArray:[_detailViewController.navigationController viewControllers]]; [viewControllers removeLastObject];[viewControllers addObject:newVC]; [_detailViewController.navigationController setViewControllers:viewControllers animated:YES];}
LAOMUSIC ARTS
Ce que je cherchais.
JERC
9

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

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 changé la conservation / libération en une conservation / libération automatique, car c'est généralement plus robuste - si une exception se produit entre la conservation / la libération, vous ferez une fuite, mais la libération automatique s'en charge.

Lily Ballard
la source
7

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

Je pense qu'une fois que le contrôleur de vue actuel est supprimé 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];

la source
Cela me donne un tout noir!
ARTS LAOMUSIQUES
4

Merci, c'était exactement ce dont j'avais besoin. J'ai également mis ceci dans une animation pour obtenir la page curl:

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

La durée 0.6 est rapide, bonne pour 3GS et plus récent, 0.8 est encore un peu trop rapide pour la 3G.

Johan

Johan
la source
Votre code est exactement ce que j'ai utilisé, super! Merci. Une remarque: avec la transition de page curl, j'ai un artifice blanc en bas de la vue (qui sait pourquoi) mais avec le retournement, cela a bien fonctionné. Quoi qu'il en soit, c'est un code sympa et compact!
David H
3

Si vous souhaitez afficher un autre contrôleur de vue par popToRootViewController, vous devez procéder 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.

msmq
la source
1

J'ai dû faire une chose similaire récemment et j'ai basé ma solution sur la réponse de Michaels. Dans mon cas, j'ai dû supprimer deux contrôleurs de vue de la pile de navigation, puis ajouter un nouveau contrôleur de vue. Appel

[contrôleurs removeLastObject];
deux fois, a bien fonctionné dans mon cas.

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

mattlangtree
la source
1

Cette UINavigationControllerméthode d'instance peut 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
diclophis
la source
1

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é ouvert, si c'est le cas, poussez-le.

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];
}
ezekielDFM
la source
1
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
    for(int i=0;i<controllers.count;i++){
       [controllers removeLastObject];
    }
 self.navigationController.viewControllers = controllers;
Ravi
la source
cela provoque un avertissement pour moi dans la console - Fin d'une transition de navigation dans un état inattendu. L'arborescence de la sous-vue de la barre de navigation peut être corrompue. Un moyen de le supprimer?
zaitsman
1

Ma façon préférée de le faire est avec une catégorie sur UINavigationController. Les éléments suivants devraient fonctionner:

UINavigationController + Helpers.h #import

@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, à partir de votre contrôleur de vue, vous pouvez remplacer la vue de dessus par une nouvelle par comme ceci:

[self.navigationController replaceTopViewControllerWithViewController: newController];
bbrame
la source
0

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.

Jignesh
la source
0

Pour IOS monotouch / xamarin:

à l'intérieur de 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
Nabil.A
la source
0

Alternativement,

Vous pouvez utiliser categorypour éviter self.navigationControllerd'être nilaprèspopViewControllerAnimated

il suffit d'appuyer et d'appuyer, c'est facile à comprendre, pas besoin 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);
payliu
la source
0

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

Si vous devez ouvrir le viewcontroller C et aller à B (hors de la pile) au lieu de A (celui ci-dessous C), il est possible de pousser B avant C et d'avoir les 3 sur la pile. En gardant la poussée B invisible et en choisissant de faire apparaître 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 (poussé invisible) -> C (quand je pop C, je choisis d'afficher B ou aussi de le faire apparaître)

alasker
la source
0

J'utilise cette solution pour conserver 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];
code4j
la source