Detectando cuando se presiona el botón ‘atrás’ en una barra de navegación

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

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

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. La detección de cuándo se está willMove(toParentViewController:) un controlador de vista es posible anulando willMove(toParentViewController:) . La idea básica es que un controlador de vista se está reventando cuando parent es nil .

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


Desde iOS 5 he encontrado que la forma más fácil de manejar 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 - (BOOL)isMovingFromParentViewController haces saltar los controladores en una stack de navegación.

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

 - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if (self.isBeingDismissed) { // Do your stuff here } } 

Como se señala 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 dependen de la existencia de UINavigationBar . Me gusta más este enfoque porque desacopla las tareas necesarias para realizar desde la acción que desencadenó el evento, es decir, al presionar un botón Atrás.

Mientras se viewWillAppear() y viewDidDisappear() cuando se viewDidDisappear() el botón Atrás, también se llaman en otro momento. Consulte el final de la respuesta para obtener más información al respecto.

Usando UIViewController.parent

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

Si el elemento primario es nulo, el controlador de vista se elimina de la stack de navegación y se descarta. Si parent no es nil, se agrega a la stack 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(toParentViewController parent: UIViewController?) { super.willMove(toParentViewController:parent) if parent == nil { // The back button was pressed or interactive gesture used } } 

Cambie willMove por didMove y compruebe self.parent para hacer el trabajo después de cerrar el controlador de vista.

Detener el despido

Tenga en cuenta, comprobar que el padre no le permite “pausar” la transición si necesita hacer algún tipo de guardado asincrónico. Para hacer eso podrías implementar lo siguiente. El único inconveniente aquí es que pierdes el elegante botón retroiluminado / 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 en vista aparecerá / apareció

Si no obtuvo el viewWillAppear viewDidDisappear viewWillAppear viewDidDisappear un ejemplo. Supongamos 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 el detailVC medida que avanzamos de la listVC a settingsVC y de vuelta a listVC

Lista> Detalle ( detalle pushVC) Detail.viewDidAppear <- aparece
Detalle> Configuración (configuración Detail.viewDidDisappear ) Detail.viewDidDisappear <- desaparecer

Y a medida que retrocedemos …
Configuraciones> Detalles (configuraciones Detail.viewDidAppear ) Detail.viewDidAppear <- aparecen
Detalle> Lista ( detalle popVC) Detail.viewDidDisappear <- desaparecer

Tenga en cuenta que se llama a viewDidDisappear varias veces, no solo cuando retrocede, sino también en el futuro. Para una operación rápida que pueda desearse, pero para una operación más compleja como una llamada de red para guardar, puede que no.

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

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

 @protocol UINavigationControllerBackButtonDelegate  /** * 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 UINavigationController recibirá una llamada a navigationBar:shouldPopItem: cada vez que se navigationBar:shouldPopItem: un controlador de vista. Allí detectamos si se presionó la parte posterior o no (cualquier otro botón). Lo único que tienes que hacer es implementar el protocolo en el controlador de vista donde se presiona Atrás.

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

Si ya ha UINavigationViewController e implementado navigationBar:shouldPopItem: no se preocupe, esto no interferirá con él.

También puede estar interesado en desactivar el gesto de atrás.

 if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) { self.navigationController.interactivePopGestureRecognizer.enabled = NO; } 

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.

Para el registro, 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]; } 

Como dice purrrminator , la respuesta de elitalon no es del todo correcta, ya que your stuff se ejecutarían incluso al abrir el controlador programáticamente.

La solución que he encontrado hasta ahora no es muy buena, pero funciona para mí. Además de lo que dijo elitalon , también elitalon 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 configurarlo en SÍ antes de hacer clic en progtwigción:

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

¡Gracias por tu ayuda!

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

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

Al usar esto, puede saber qué controlador muestra el UINavigationController.

 if ([viewController isKindOfClass:[HomeController class]]) { NSLog(@"Show home controller"); } 

La respuesta de 7ynk3r fue muy similar a la que usé 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; } } 

Debería verificar el protocolo UINavigationBarDelegate . En este caso, es posible que desee utilizar el método navigationBar: shouldPopItem:

Como dijo Coli88, debes verificar el protocolo UINavigationBarDelegate.

De una manera más general, también puede usar el - (void)viewWillDisapear:(BOOL)animated para realizar un trabajo personalizado cuando la vista retenida por el controlador de vista actualmente visible está a punto de desaparecer. Desafortunadamente, esto cubriría molestar el empuje y las cajas de sorpresas.

Para Swift con un UINavigationController:

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

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!

Quienes 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?

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 a super .

Si implementa willMove(toParent:) sin llamar a super , entonces self.isMovingFromParent será false y el uso de viewWillDisappear parecerá fallar. No falló; lo rompiste.

Puede usar la callback del botón Atrás, como esta:

 - (BOOL) navigationShouldPopOnBackButton { [self backAction]; return NO; } - (void) backAction { // your code goes here // show confirmation alert, for example // ... } 

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 } } 

(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 } } }