Jak mogę wyświetlić widok z kontrolera UINavigationController i zastąpić go innym w jednej operacji?

84

Mam aplikację, w której muszę usunąć jeden widok ze stosu UINavigationController i zastąpić go innym. Sytuacja jest taka, że ​​pierwszy widok tworzy element edytowalny, a następnie zastępuje się edytorem elementu. Kiedy robię oczywiste rozwiązanie w pierwszym widoku:

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

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

Zachowuje się bardzo dziwnie. Zwykle pojawia się widok edytora, ale jeśli spróbuję użyć przycisku Wstecz na pasku nawigacyjnym, pojawiają się dodatkowe ekrany, niektóre puste, a niektóre po prostu schrzanione. Tytuł też staje się przypadkowy. To tak, jakby stos nawigacji był całkowicie zawiązany.

Jakie byłoby lepsze podejście do tego problemu?

Dzięki, Matt

Matt Brandt
źródło

Odpowiedzi:

137

Odkryłem, że nie musisz w ogóle ręcznie bawić się viewControllersnieruchomością. Zasadniczo są w tym dwie trudne rzeczy.

  1. self.navigationControllerzwróci, niljeśli selfaktualnie nie ma go na stosie kontrolera nawigacji. Więc zapisz go w zmiennej lokalnej, zanim stracisz do niej dostęp.
  2. Musisz retain(i właściwie release) selfalbo obiekt, który jest właścicielem metody, w której się znajdujesz, zostanie zwolniony, powodując dziwność.

Gdy już to zrobisz, po prostu pop i naciśnij jak zwykle. Ten kod natychmiast zastąpi górny kontroler innym.

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

W tym ostatnim wierszu Jeśli zmienisz animatedsię YES, wówczas nowy ekran będzie faktycznie ożywione i regulator po prostu pojawiło się będą animować. Wygląda całkiem nieźle!

Alex Wayne
źródło
znakomity! znacznie lepsze rozwiązanie
emmby
Niesamowite. Chociaż nie musiałem dzwonić do [[self retain] autorelease], nadal działa dobrze.
iamj4de
4
Być może jest to oczywisty dodatek, ale możesz umieścić powyższy kod w bloku animacji, aby animować przejście: [UIView beginAnimations: @ "View Flip" context: nil]; [UIView setAnimationDuration: 0,80]; [UIView setAnimationCurve: UIViewAnimationCurveEaseInOut]; [UIView setAnimationTransition: UIViewAnimationTransitionFlipFromRight forView: navController.view cache: NO]; [navController pushViewController: newController animowany: TAK]; [UIView commitAnimations];
Martin
11
Działa świetnie z ARC, po prostu usuwając linię retain / autorelease.
Ian Terrell,
2
@TomerPeled Tak, ta odpowiedź ma prawie 5 lat ... Myślę, że tak było w przypadku iOS 3. API zmieniły się na tyle, że nie jestem już pewien, czy jest to najlepsza odpowiedź.
Alex Wayne
56

Poniższe podejście wydaje mi się przyjemniejsze i działa również dobrze z 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
źródło
1
@LukeRogers, powoduje to dla mnie następujące ostrzeżenie: Kończenie przejścia nawigacji w nieoczekiwanym stanie. Drzewo podglądu paska nawigacji może zostać uszkodzone. Jakiś sposób to stłumić?
zaitsman
Korzystając z tego rozwiązania, nadpisujesz popover. Aby pokazać w widoku DetailView, kod powinien brzmieć: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];}
SZTUKI LAOMUZYCZNE
Czego szukałem.
JERC
9

Z doświadczenia wynika, że ​​będziesz musiał viewControllersbezpośrednio bawić się własnością UINavigationControllera . Coś takiego powinno działać:

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

Uwaga: Zmieniłem zachowanie / zwolnienie na zachowaj / zwolnij, ponieważ jest to po prostu bardziej niezawodne - jeśli wystąpi wyjątek między zatrzymaniem / zwolnieniem, wyciekniesz samoczynnie, ale autorelease się tym zajmą.

Lily Ballard
źródło
7

Po wielu wysiłkach (i poprawieniu kodu od Kevina) w końcu wymyśliłem, jak to zrobić w kontrolerze widoku, który jest zdejmowany ze stosu. Problem, który miałem, polegał na tym, że self.navigationController zwracało zero po usunięciu ostatniego obiektu z tablicy controllers. Myślę, że było to spowodowane tym wierszem w dokumentacji dla UIViewController w metodzie wystąpienia navigationController „Zwraca kontroler nawigacji tylko wtedy, gdy kontroler widoku jest na stosie”.

Myślę, że gdy bieżący kontroler widoku zostanie usunięty ze stosu, jego metoda navigationController zwróci nil.

Oto dostosowany kod, który działa:

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

źródło
To daje mi czarną całość!
SZTUKI LAOMUZYCZNE
4

Dzięki, właśnie tego potrzebowałem. Umieściłem to również w animacji, aby uzyskać zawinięcie strony:

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

Czas trwania 0.6 jest szybki, dobry dla 3GS i nowszych, 0.8 to wciąż trochę za szybko dla 3G.

Johan

Johan
źródło
Twój kod jest dokładnie tym, czego użyłem, świetnie! Dzięki. Jedna uwaga: z przejściem z zawijaniem strony dostałem biały sztuczny u dołu widoku (kto wie dlaczego), ale z odwróceniem działało dobrze. W każdym razie to ładny i kompaktowy kod!
David H
3

Jeśli chcesz wyświetlić inny kontroler widoku przez popToRootViewController, musisz wykonać następujące czynności:

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

Teraz cały twój poprzedni stos zostanie usunięty, a nowy stos zostanie utworzony z wymaganym rootViewController.

msmq
źródło
1

Ostatnio musiałem zrobić coś podobnego i oparłem swoje rozwiązanie na odpowiedzi Michaelsa. W moim przypadku musiałem usunąć dwa kontrolery widoku ze stosu nawigacji, a następnie dodać nowy kontroler widoku. Powołanie

[kontrolery removeLastObject];
dwa razy, działało dobrze w moim przypadku.

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
źródło
1

Ta UINavigationControllermetoda instancji może działać ...

Wyświetla kontrolery widoku do momentu, gdy określony kontroler widoku stanie się kontrolerem widoku z góry, a następnie zaktualizuje wyświetlacz.

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
diclophis
źródło
1

Oto inne podejście, które nie wymaga bezpośredniego mieszania się z tablicą viewControllers. Sprawdź, czy kontroler został już wyskakujący, jeśli tak, naciśnij go.

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
źródło
1
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
    for(int i=0;i<controllers.count;i++){
       [controllers removeLastObject];
    }
 self.navigationController.viewControllers = controllers;
Ravi
źródło
powoduje to ostrzeżenie w konsoli - Kończenie przejścia nawigacji w nieoczekiwanym stanie. Drzewo podglądu paska nawigacji może zostać uszkodzone. Jakiś sposób to stłumić?
zaitsman
1

Moim ulubionym sposobem jest dodanie kategorii na UINavigationController. Powinno działać:

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

Następnie z kontrolera widoku możesz zastąpić widok z góry nowym, wykonując następujące czynności:

[self.navigationController replaceTopViewControllerWithViewController: newController];
bbrame
źródło
0

Możesz sprawdzić za pomocą tablicy kontrolerów widoku nawigacji, którą dajesz wszystkie kontrolery widoku, które dodałeś w stosie nawigacji. Używając tej tablicy, możesz z powrotem przejść do określonego kontrolera widoku.

jignesh
źródło
0

W przypadku monotouch / Xamarin IOS:

wewnątrz klasy 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
źródło
0

Alternatywnie,

Możesz użyć, categoryaby uniknąć self.navigationControllerbyć nilpopopViewControllerAnimated

po prostu pop i naciśnij, jest to łatwe do zrozumienia, nie trzeba uzyskiwać dostępu 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

W Twoim ViewController

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

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

RELEASE_SAFELY(v);
payliu
źródło
0

Nie do końca odpowiedź, ale może być pomocna w niektórych scenariuszach (na przykład mój):

Jeśli chcesz zdjąć kontroler widoku C i przejść do B (poza stosem) zamiast A (ten poniżej C), możesz wcisnąć B przed C i mieć wszystkie 3 na stosie. Utrzymując przycisk B niewidocznym, i wybierając, czy wyskakuje tylko C, czy C i B razem, możesz osiągnąć ten sam efekt.

początkowy problem A -> C (chcę zdjąć C i pokazać B, poza stosem)

możliwe rozwiązanie A -> B (wciśnięte niewidoczne) -> C (kiedy wyskakuję C, wybieram pokazanie B lub też pęknięcie)

alasker
źródło
0

Używam tego rozwiązania do zachowania animacji.

[self.navigationController pushViewController:controller animated:YES];
NSMutableArray *newControllers = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
[newControllers removeObject:newControllers[newControllers.count - 2]];
[self.navigationController setViewControllers:newControllers];
code4j
źródło