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.
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.
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.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!
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];
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.
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];
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
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.
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];
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
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
for(int i=0;i<controllers.count;i++){
[controllers removeLastObject];
}
self.navigationController.viewControllers = controllers;
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];
}
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>
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
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)
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.
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];
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);