¿Cómo puedo abrir una vista de un UINavigationController y reemplazarla con otra en una operación?

84

Tengo una aplicación en la que necesito eliminar una vista de la pila de un UINavigationController y reemplazarla por otra. La situación es que la primera vista crea un elemento editable y luego se reemplaza con un editor para el elemento. Cuando hago la solución obvia en la primera vista:

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

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

Tengo un comportamiento muy extraño. Por lo general, aparecerá la vista del editor, pero si trato de usar el botón Atrás en la barra de navegación, obtengo pantallas adicionales, algunas en blanco y otras simplemente arruinadas. El título también se vuelve aleatorio. Es como si la pila de navegación estuviera completamente regada.

¿Cuál sería un mejor enfoque para este problema?

Gracias Matt

Matt Brandt
fuente

Respuestas:

137

Descubrí que no es necesario que te metas manualmente con la viewControllerspropiedad. Básicamente, hay 2 cosas complicadas sobre esto.

  1. self.navigationControllervolverá nilsi selfno está actualmente en la pila del controlador de navegación. Así que guárdelo en una variable local antes de perder el acceso a él.
  2. Debe retain(y correctamente release) selfo el objeto que posee el método en el que se encuentra será desasignado, lo que causará extrañeza.

Una vez que haya hecho esa preparación, simplemente haga estallar y presione como de costumbre. Este código reemplazará instantáneamente el controlador superior por otro.

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

En esa última línea, si cambia animateda YES, entonces la nueva pantalla se animará y el controlador que acaba de abrir se animará. ¡Se ve muy bien!

Alex Wayne
fuente
¡brillante! solución mucho mejor
emmby
Increíble. Aunque no necesité llamar a [[self retener] autorelease], todavía funciona bien.
iamj4de
4
Quizás una adición obvia, pero luego puede poner el código de arriba en un bloque de animación para animar la transición: [UIView beginAnimations: @ "View Flip" context: nil]; [UIView setAnimationDuration: 0.80]; [UIView setAnimationCurve: UIViewAnimationCurveEaseInOut]; [UIView setAnimationTransition: UIViewAnimationTransitionFlipFromRight forView: navController.view cache: NO]; [navController pushViewController: newController animado: SÍ]; [UIView commitAnimations];
Martin
11
Funciona muy bien con ARC simplemente eliminando la línea de retención / liberación automática.
Ian Terrell
2
@TomerPeled Sí, esta respuesta tiene casi 5 años ... Creo que ese fue el caso en iOS 3. Las API han cambiado lo suficiente como para que no esté seguro de que sea la mejor respuesta.
Alex Wayne
56

El siguiente enfoque me parece más agradable y también funciona bien con 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
fuente
1
@LukeRogers, esto me provoca la siguiente advertencia: Terminando una transición de navegación en un estado inesperado. El árbol de la subvista de la barra de navegación podría dañarse. ¿Alguna forma de reprimirlo?
zaitsman
Con esta solución, sobrescribe el popover. Y para mostrar en DetailView, su código debe leer: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];}
ARTES LAOMUSICAS
Lo que buscaba.
JERC
9

Por experiencia, tendrá que jugar viewControllersdirectamente con la propiedad de UINavigationController . Algo como esto debería funcionar:

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

Nota: Cambié la retención / liberación a retención / liberación automática, ya que generalmente es más robusta: si ocurre una excepción entre la retención / liberación, se filtrará usted mismo, pero la liberación automática se encarga de eso.

Lily Ballard
fuente
7

Después de mucho esfuerzo (y de modificar el código de Kevin), finalmente descubrí cómo hacer esto en el controlador de vista que se está extrayendo de la pila. El problema que estaba teniendo era que self.navigationController devolvía nil después de que eliminé el último objeto de la matriz de controladores. Creo que se debió a esta línea en la documentación de UIViewController en el método de instancia navigationController "Solo devuelve un controlador de navegación si el controlador de vista está en su pila".

Creo que una vez que el controlador de vista actual se elimina de la pila, su método navigationController devolverá nil.

Aquí está el código ajustado que funciona:

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

fuente
¡Esto me da un todo negro!
ARTES LAOMUSICAS
4

Gracias, esto era exactamente lo que necesitaba. También puse esto en una animación para obtener la curvatura de la página:

        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 de duración es rápido, bueno para 3GS y versiones posteriores, 0,8 sigue siendo un poco demasiado rápido para 3G.

Johan

Johan
fuente
Tu código es exactamente lo que usé, ¡genial! Gracias. Una nota: con la transición de curvatura de la página obtuve un artificio blanco en la parte inferior de la vista (quién sabe por qué) pero con la rotación funcionó bien. De todos modos, ¡este es un código agradable y compacto!
David H
3

Si desea mostrar cualquier otro controlador de vista por popToRootViewController, debe hacer lo siguiente:

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

Ahora, se eliminará toda su pila anterior y se creará una nueva pila con su rootViewController requerido.

msmq
fuente
1

Tuve que hacer algo similar recientemente y basé mi solución en la respuesta de Michaels. En mi caso, tuve que eliminar dos controladores de vista de la pila de navegación y luego agregar un nuevo controlador de vista. Vocación

[controladores removeLastObject];
dos veces, funcionó bien en mi caso.

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
fuente
1

Este UINavigationControllermétodo de instancia podría funcionar ...

Aparece los controladores de vista hasta que el controlador de vista especificado sea el controlador de vista superior y luego actualice la pantalla.

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
diclophis
fuente
1

Aquí hay otro enfoque que no requiere jugar directamente con la matriz viewControllers. Verifique si el controlador ya ha aparecido, si es así, presiónelo.

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
fuente
1
NSMutableArray *controllers = [self.navigationController.viewControllers mutableCopy];
    for(int i=0;i<controllers.count;i++){
       [controllers removeLastObject];
    }
 self.navigationController.viewControllers = controllers;
Ravi
fuente
esto me provoca una advertencia en la consola: finalizando una transición de navegación en un estado inesperado. El árbol de la subvista de la barra de navegación podría dañarse. ¿Alguna forma de reprimirlo?
zaitsman
1

Mi forma favorita de hacerlo es con una categoría en UINavigationController. Lo siguiente debería funcionar:

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

Luego, desde su controlador de vista, puede reemplazar la vista superior con una nueva de esta manera:

[self.navigationController replaceTopViewControllerWithViewController: newController];
bbrame
fuente
0

Puede verificar con la matriz de controladores de vista de navegación que le brinda todos los controladores de vista que ha agregado en la pila de navegación. Al usar esa matriz, puede volver a navegar al controlador de vista específico.

jignesh
fuente
0

Para IOS monotouch / xamarin:

dentro de la clase 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
fuente
0

Alternativamente,

Puedes usar categorypara evitar self.navigationControllersernil despuéspopViewControllerAnimated

simplemente abre y presiona, es fácil de entender, no es necesario acceder 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

En su ViewController

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

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

RELEASE_SAFELY(v);
payliu
fuente
0

No es exactamente la respuesta, pero podría ser de ayuda en algunos escenarios (el mío, por ejemplo):

Si necesita abrir el controlador de vista C e ir a B (fuera de la pila) en lugar de A (el que está debajo de C), es posible presionar B antes de C y tener los 3 en la pila. Al mantener invisible el empuje B, y al elegir si hacer estallar solo C o C y B por completo, puede lograr el mismo efecto.

problema inicial A -> C (quiero sacar C y mostrar B, fuera de la pila)

posible solución A -> B (empujado invisible) -> C (cuando abro C, elijo mostrar B o también hacerlo)

alasker
fuente
0

Utilizo esta solución para mantener la animación.

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