Detectar cuando se presiona el botón 'atrás' en una barra de navegación

135

Necesito realizar algunas acciones cuando el botón Atrás (volver a la pantalla anterior, volver a la vista principal) se presiona en una barra de navegación.

¿Hay algún método que pueda implementar para capturar el evento y activar algunas acciones para pausar y guardar datos antes de que desaparezca la pantalla?

ewok
fuente
posible duplicado de la acción
nielsbot
1
Mira la solución en este hilo
Jiri Volejnik
Lo hice de esta manera mostrar decisión aquí
Taras

Respuestas:

316

ACTUALIZACIÓN: Según algunos comentarios, la solución en la respuesta original no parece funcionar en ciertos escenarios en iOS 8+. No puedo verificar que ese sea realmente el caso sin más detalles.

Para aquellos de ustedes, sin embargo, en esa situación hay una alternativa. Es posible detectar cuándo se abre un controlador de vista anulando willMove(toParentViewController:). La idea básica es que se abre un controlador de vista cuando parentes así nil.

Consulte "Implementación de un controlador de vista de contenedor" para obtener más detalles.


Desde iOS 5 descubrí que la forma más fácil de lidiar con esta situación es usar el nuevo método - (BOOL)isMovingFromParentViewController:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController) {
    // Do your stuff here
  }
}

- (BOOL)isMovingFromParentViewController tiene sentido cuando empuja y hace estallar controladores en una pila de navegación.

Sin embargo, si presenta controladores de vista modal, debe usar - (BOOL)isBeingDismisseden su lugar:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isBeingDismissed) {
    // Do your stuff here
  }
}

Como se señaló en esta pregunta , puede combinar ambas propiedades:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if (self.isMovingFromParentViewController || self.isBeingDismissed) {
    // Do your stuff here
  }
}

Otras soluciones se basan en la existencia de a UINavigationBar. En cambio, me gusta más mi enfoque porque desacopla las tareas requeridas para realizar de la acción que activó el evento, es decir, presionar un botón de retroceso.

elitalon
fuente
Me gusta tu respuesta. ¿Pero por qué usaste 'self.isBeingDismissed'? En mi caso, las declaraciones en 'self.isBeingDismissed' no se implementan.
Rutvij Kotecha
3
self.isMovingFromParentViewControllertiene un valor VERDADERO cuando estoy desplegando la pila de navegación mediante programación popToRootViewControllerAnimated, sin tocar el botón Atrás. ¿Debería rechazar su respuesta? (el sujeto dice "botón 'atrás' se presiona en una barra de navegación")
kas-kad
2
Excelente respuesta, muchas gracias. En Swift usé:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }
Camillo Visini
1
Solo debe hacer esto dentro, -viewDidDisappear:ya que es posible que obtenga un -viewWillDisappear:sin un -viewDidDisappear:(como cuando comienza a deslizar para descartar un elemento del controlador de navegación y luego cancelar ese deslizamiento.)
Heath Borders
3
Parece que ya no es una solución confiable. Funcionó en el momento en que usé esto por primera vez (era iOS 10). Pero ahora accidentalmente descubrí que con calma dejó de funcionar (iOS 11). Tuve que cambiar a la solución "willMove (toParentViewController)".
Vitalii
100

Mientras viewWillAppear()y viewDidDisappear() se llaman cuando se toca el botón Atrás, también se llaman en otros momentos. Vea el final de la respuesta para más información sobre eso.

Usando UIViewController.parent

La detección del botón Atrás se realiza mejor cuando el VC se elimina de su padre (el NavigationController) con la ayuda de willMoveToParentViewController(_:)ORdidMoveToParentViewController()

Si padre es nulo, el controlador de vista se saca de la pila de navegación y se descarta. Si padre no es nulo, se agrega a la pila y se presenta.

// Objective-C
-(void)willMoveToParentViewController:(UIViewController *)parent {
     [super willMoveToParentViewController:parent];
    if (!parent){
       // The back button was pressed or interactive gesture used
    }
}


// Swift
override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent)
    if parent == nil {
        // The back button was pressed or interactive gesture used
    }
}

Intercambiar willMovepara didMovey verificación self.parent para hacer el trabajo después del controlador de vista es despedido.

Deteniendo el despido

Tenga en cuenta que verificar el padre no le permite "pausar" la transición si necesita hacer algún tipo de guardado asíncrono. Para hacer eso, podría implementar lo siguiente. El único inconveniente aquí es que pierdes el elegante botón de retroceso animado / estilo iOS. También tenga cuidado aquí con el gesto de deslizamiento interactivo. Use lo siguiente para manejar este caso.

var backButton : UIBarButtonItem!

override func viewDidLoad() {
    super.viewDidLoad()

     // Disable the swipe to make sure you get your chance to save
     self.navigationController?.interactivePopGestureRecognizer.enabled = false

     // Replace the default back button
    self.navigationItem.setHidesBackButton(true, animated: false)
    self.backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "goBack")
    self.navigationItem.leftBarButtonItem = backButton
}

// Then handle the button selection
func goBack() {
    // Here we just remove the back button, you could also disabled it or better yet show an activityIndicator
    self.navigationItem.leftBarButtonItem = nil
    someData.saveInBackground { (success, error) -> Void in
        if success {
            self.navigationController?.popViewControllerAnimated(true)
            // Don't forget to re-enable the interactive gesture
            self.navigationController?.interactivePopGestureRecognizer.enabled = true
        }
        else {
            self.navigationItem.leftBarButtonItem = self.backButton
            // Handle the error
        }
    }
}


Más sobre la vista aparecerá / apareció

Si no obtuvo el viewWillAppear viewDidDisappearproblema, veamos un ejemplo. Digamos que tiene tres controladores de vista:

  1. ListVC: una vista de tabla de cosas
  2. DetailVC: detalles sobre una cosa
  3. SettingsVC: algunas opciones para algo

Vamos a seguir las llamadas en la detailVCmedida que avanza de la listVCa settingsVCy de nuevo alistVC

List> Detail (push detailVC) Detail.viewDidAppear<- aparece
Detail> Settings (push settingsVC) Detail.viewDidDisappear<- desaparecer

Y a medida que retrocedemos ...
Configuración> Detalle (pop settingsVC) Detail.viewDidAppear<- aparece
Detalle> Lista (pop detailVC) Detail.viewDidDisappear<- desaparece

Tenga en cuenta que viewDidDisappearse llama varias veces, no solo al retroceder, sino también al avanzar. Para una operación rápida que puede desearse, pero para una operación más compleja como una llamada de red para guardar, puede que no.

WCByrne
fuente
Solo una nota, el usuario didMoveToParantViewController:debe trabajar cuando la vista ya no es visible. Útil para iOS7 con el InteractiveGesutre
WCByrne
didMoveToParentViewController * hay un error tipográfico
thewormsterror
¡No olvides llamar a [super willMoveToParentViewController: parent]!
ScottyB
2
El parámetro primario es nulo cuando está apareciendo en el controlador de vista principal, y no nulo cuando se muestra la vista en la que aparece este método. Puede usar ese hecho para realizar una acción solo cuando se presiona el botón Atrás, y no cuando se llega a la vista. Esa fue, después de todo, la pregunta original. :)
Mike
1
Esto también se llama cuando se usa mediante programación _ = self.navigationController?.popViewController(animated: true), por lo que no solo se llama al presionar el botón Atrás. Estoy buscando una llamada que funcione solo cuando se presiona Atrás.
Ethan Allen el
16

Primer método

- (void)didMoveToParentViewController:(UIViewController *)parent
{
    if (![parent isEqual:self.parentViewController]) {
         NSLog(@"Back pressed");
    }
}

Segundo método

-(void) viewWillDisappear:(BOOL)animated {
    if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
       // back button was pressed.  We know this is true because self is no longer
       // in the navigation stack.  
    }
    [super viewWillDisappear:animated];
}
Zar E Ahmer
fuente
1
El segundo método fue el único que funcionó para mí. El primer método también fue llamado cuando se presentó mi punto de vista, lo cual no era aceptable para mi caso de uso.
marcshilling
10

Los que afirman que esto no funciona están equivocados:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    if self.isMovingFromParent {
        print("we are being popped")
    }
}

Eso funciona bien Entonces, ¿qué está causando el mito generalizado de que no es así?

El problema parece deberse a una implementación incorrecta de un método diferente , a saber, que la implementación de willMove(toParent:)Olvidó llamar super.

Si implementa willMove(toParent:)sin llamar super, entonces self.isMovingFromParentserá falsey el uso de viewWillDisappearparecerá fallar. No falló; lo rompiste.

NOTA: El problema real suele ser que el segundo controlador de vista detecte que el primer controlador de vista apareció. Consulte también la discusión más general aquí: ¿La detección unificada de UIViewController "se convirtió en la primera"?

EDITAR Un comentario sugiere que esto debería ser viewDidDisappearmás que viewWillDisappear.

mate
fuente
Este código se ejecuta cuando se toca el botón Atrás, pero también se ejecuta si el VC se abre programáticamente.
biomiker
@biomiker Claro, pero eso también sería cierto para los otros enfoques. Hacer estallar es estallar. La pregunta es cómo detectar un pop cuando no lo hiciste programáticamente. Si aparece pop programáticamente, ya sabe que está apareciendo, por lo que no hay nada que detectar.
mate
Sí, esto es cierto para varios de los otros enfoques y muchos de ellos tienen comentarios similares. Solo estaba aclarando, ya que esta era una respuesta reciente con una refutación específica y había tenido muchas esperanzas cuando la leí. Sin embargo, para el registro, la pregunta es cómo detectar una presión del botón Atrás. Es un argumento razonable decir que el código que también se ejecutará en situaciones en las que no se presiona el botón Atrás, sin indicar si se presionó o no el botón Atrás, no resuelve completamente la pregunta real, incluso si tal vez la pregunta podría haber sido más explícito sobre ese punto.
biomiker
1
Desafortunadamente, esto regresa truepara el gesto emergente de deslizamiento interactivo, desde el borde izquierdo del controlador de vista, incluso si el deslizamiento no lo destacó por completo. Entonces, en lugar de registrarlo willDisappear, hacerlo didDisappearfunciona.
badhanganesh
1
@badhanganesh Gracias, respuesta editada para incluir esa información.
mate
9

He jugado (o peleado) con este problema durante dos días. En mi opinión, el mejor enfoque es crear una clase de extensión y un protocolo, como este:

@protocol UINavigationControllerBackButtonDelegate <NSObject>
/**
 * Indicates that the back button was pressed.
 * If this message is implemented the pop logic must be manually handled.
 */
- (void)backButtonPressed;
@end

@interface UINavigationController(BackButtonHandler)
@end

@implementation UINavigationController(BackButtonHandler)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;
    SEL backButtonPressedSel = @selector(backButtonPressed);
    if (wasBackButtonClicked && [topViewController respondsToSelector:backButtonPressedSel]) {
        [topViewController performSelector:backButtonPressedSel];
        return NO;
    }
    else {
        [self popViewControllerAnimated:YES];
        return YES;
    }
}
@end

Esto funciona porque UINavigationControllerrecibirá una llamada navigationBar:shouldPopItem:cada vez que aparezca un controlador de vista. Allí detectamos si se presionó la tecla de retroceso o no (cualquier otro botón). Lo único que tiene que hacer es implementar el protocolo en el controlador de vista donde se presiona la parte posterior.

Recuerde abrir manualmente el controlador de vista backButtonPressedSel, si todo está bien.

Si ya ha subclasificado UINavigationViewControllere implementado navigationBar:shouldPopItem:, no se preocupe, esto no interferirá con ello.

También te puede interesar desactivar el gesto de retroceso.

if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {
    self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
7ynk3r
fuente
1
Esta respuesta fue casi completa para mí, excepto que encontré que a menudo se soltaban 2 controladores de vista. Devolver SÍ hace que el método de llamada llame a pop, por lo que llamar a pop también significa que se abrirán 2 viewcontrollers. Vea esta respuesta en otra pregunta para obtener más detalles (una muy buena respuesta que merece más votos a favor): stackoverflow.com/a/26084150/978083
Jason Ridge
Buen punto, mi descripción no estaba clara sobre ese hecho. El "Recuerde abrir manualmente el controlador de vista si todo está bien" es solo para el caso de devolver "NO", de lo contrario, el flujo es el pop normal.
7ynk3r
1
Para la rama "else", es mejor llamar a la súper implementación si no desea manejar el pop usted mismo y dejar que devuelva lo que considere correcto, que es principalmente SÍ, pero también se encarga del pop y anima el chevron correctamente .
Ben Sinclair
9

Esto funciona para mí en iOS 9.3.x con Swift:

override func didMoveToParentViewController(parent: UIViewController?) {
    super.didMoveToParentViewController(parent)

    if parent == self.navigationController?.parentViewController {
        print("Back tapped")
    }
}

A diferencia de otras soluciones aquí, esto no parece desencadenarse inesperadamente.

Chris Villa
fuente
es mejor usar willMove en su lugar
Eugene Gordin
4

Para que conste, creo que esto es más de lo que estaba buscando ...

    UIBarButtonItem *l_backButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:@selector(backToRootView:)];

    self.navigationItem.leftBarButtonItem = l_backButton;


    - (void) backToRootView:(id)sender {

        // Perform some custom code

        [self.navigationController popToRootViewControllerAnimated:YES];
    }
Paul Brady
fuente
1
Gracias Paul, esta solución es bastante simple. Desafortunadamente, el ícono es diferente. Este es el ícono "rebobinar", no el ícono atrás. Tal vez hay una manera de usar el icono de atrás ...
Ferran Maylinch
2

Como se purrrminatordice, la respuesta elitalonno es del todo correcta, ya your stuffque se ejecutaría incluso cuando se activa el controlador mediante programación.

La solución que he encontrado hasta ahora no es muy agradable, pero funciona para mí. Además de lo elitalondicho, también verifico si estoy apareciendo programáticamente o no:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

Debe agregar esa propiedad a su controlador y establecerla en SÍ antes de aparecer mediante programación:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];

¡Gracias por tu ayuda!

Ferran Maylinch
fuente
2

La mejor manera es usar los métodos de delegado UINavigationController

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated

Con esto, puede saber qué controlador muestra el UINavigationController.

if ([viewController isKindOfClass:[HomeController class]]) {
    NSLog(@"Show home controller");
}
Harald
fuente
¡Esto debe ser marcado como la respuesta correcta! También podría querer agregar una línea más solo para recordarle a la gente -> self.navigationController.delegate = self;
Mike Critchley
2

He resuelto este problema agregando un UIControl a la barra de navegación en el lado izquierdo.

UIControl *leftBarItemControl = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 90, 44)];
[leftBarItemControl addTarget:self action:@selector(onLeftItemClick:) forControlEvents:UIControlEventTouchUpInside];
self.leftItemControl = leftBarItemControl;
[self.navigationController.navigationBar addSubview:leftBarItemControl];
[self.navigationController.navigationBar bringSubviewToFront:leftBarItemControl];

Y debe recordar eliminarlo cuando la vista desaparezca:

- (void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.leftItemControl) {
        [self.leftItemControl removeFromSuperview];
    }    
}

¡Eso es todo!

Eric
fuente
2

Puede usar la devolución de llamada del botón Atrás, de esta manera:

- (BOOL) navigationShouldPopOnBackButton
{
    [self backAction];
    return NO;
}

- (void) backAction {
    // your code goes here
    // show confirmation alert, for example
    // ...
}

para la versión rápida, puede hacer algo como en el ámbito global

extension UIViewController {
     @objc func navigationShouldPopOnBackButton() -> Bool {
     return true
    }
}

extension UINavigationController: UINavigationBarDelegate {
     public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
          return self.topViewController?.navigationShouldPopOnBackButton() ?? true
    }
}

Debajo de uno pones en el controlador de vista donde quieres controlar la acción del botón Atrás:

override func navigationShouldPopOnBackButton() -> Bool {
    self.backAction()//Your action you want to perform.

    return true
}
Pedro Magalhães
fuente
1
No sé por qué alguien rechazó la votación. Esta parece ser, con mucho, la mejor respuesta.
Avinash
@Avinash ¿De dónde navigationShouldPopOnBackButtonviene? No es parte de la API pública.
elitalon
@elitalon Lo siento, esta fue la mitad de la respuesta. Pensé que el contexto restante estaba allí en cuestión. De todos modos he actualizado la respuesta ahora
Avinash
1

Como dijo Coli88, debe verificar el protocolo UINavigationBarDelegate.

De una manera más general, también puede usar - (void)viewWillDisapear:(BOOL)animatedpara realizar un trabajo personalizado cuando la vista retenida por el controlador de vista actualmente visible está a punto de desaparecer. Desafortunadamente, esto cubriría tanto los casos de empuje como los de pop.

ramdam
fuente
1

Para Swift con un UINavigationController:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
fuente
1

La respuesta de 7ynk3r estuvo muy cerca de lo que utilicé al final, pero necesitaba algunos ajustes:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

    UIViewController *topViewController = self.topViewController;
    BOOL wasBackButtonClicked = topViewController.navigationItem == item;

    if (wasBackButtonClicked) {
        if ([topViewController respondsToSelector:@selector(navBackButtonPressed)]) {
            // if user did press back on the view controller where you handle the navBackButtonPressed
            [topViewController performSelector:@selector(navBackButtonPressed)];
            return NO;
        } else {
            // if user did press back but you are not on the view controller that can handle the navBackButtonPressed
            [self popViewControllerAnimated:YES];
            return YES;
        }
    } else {
        // when you call popViewController programmatically you do not want to pop it twice
        return YES;
    }
}
micromanc3r
fuente
0

self.navigationController.isMovingFromParentViewController ya no funciona en iOS8 y 9 Yo uso:

-(void) viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if (self.navigationController.topViewController != self)
    {
        // Is Popping
    }
}
Vastamente
fuente
-1

(RÁPIDO)

finalmente encontramos la solución ... el método que estábamos buscando es "willShowViewController", que es el método delegado de UINavigationController

//IMPORT UINavigationControllerDelegate !!
class PushedController: UIViewController, UINavigationControllerDelegate {

    override func viewDidLoad() {
        //set delegate to current class (self)
        navigationController?.delegate = self
    }

    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
        //MyViewController shoud be the name of your parent Class
        if var myViewController = viewController as? MyViewController {
            //YOUR STUFF
        }
    }
}
Jiří Zahálka
fuente
1
El problema con este enfoque es que se acopla MyViewControllera PushedController.
clozach