devolución de llamada del botón de retroceso en navigationController en iOS

102

He insertado una vista en el controlador de navegación y, cuando presiono el botón Atrás, pasa automáticamente a la vista anterior. Quiero hacer algunas cosas cuando se presiona el botón Atrás antes de sacar la vista de la pila. ¿Cuál es la función de devolución de llamada del botón Atrás?

Namratha
fuente
posible duplicado de la acción
Zakaria
Consulte esta [solución] [1] que también conserva el estilo del botón de retroceso. [1]: stackoverflow.com/a/29943156/3839641
Sarasranglt

Respuestas:

162

La respuesta de William Jockusch resuelve este problema con un truco fácil.

-(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];
}
ymutlu
fuente
32
Este código no solo se ejecuta cuando el usuario presiona el botón Atrás, sino que en cada evento se abre la vista (por ejemplo, cuando se tiene un botón de hacer o guardar en el lado derecho).
significado importa
7
O al avanzar a una nueva vista.
GuybrushThreepwood
Esto también se llama cuando el usuario hace una panorámica desde el borde izquierdo (interactivePopGestureRecognizer). En mi caso, estoy buscando específicamente cuando el usuario presiona hacia atrás mientras NO se desplaza desde el borde izquierdo.
Kyle Clegg
2
No significa que el botón de retroceso haya sido la causa. Podría ser una transición para relajarse, por ejemplo.
smileBot
1
Tengo una duda, ¿por qué no deberíamos hacer esto en viewDidDisappear?
JohnVanDijk
85

En mi opinión la mejor solución.

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

Pero solo funciona con iOS5 +

Blanco
fuente
3
Esta técnica no puede distinguir entre un toque de botón de retroceso y una secuencia de relajación.
smileBot
El método willMoveToParentViewController y viewWillDisappear no explica que el controlador debe ser destruido, didMoveToParentViewController es correcto
Hank
27

Probablemente sea mejor anular el botón trasero para que pueda manejar el evento antes de que aparezca la vista para cosas como la confirmación del usuario.

en viewDidLoad crea un UIBarButtonItem y establece self.navigationItem.leftBarButtonItem en él pasando un sel

- (void) viewDidLoad
{
// change the back button to cancel and add an event handler
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:@”back
style:UIBarButtonItemStyleBordered
target:self
action:@selector(handleBack:)];

self.navigationItem.leftBarButtonItem = backButton;
[backButton release];

}
- (void) handleBack:(id)sender
{
// pop to root view controller
[self.navigationController popToRootViewControllerAnimated:YES];

}

Luego, puede hacer cosas como generar un UIAlertView para confirmar la acción, luego abrir el controlador de vista, etc.

O en lugar de crear un nuevo botón trasero, puede ajustarse a los métodos delegados de UINavigationController para realizar acciones cuando se presiona el botón Atrás.

roocell
fuente
El UINavigationControllerDelegateno tiene métodos que son llamados cuando se pulsa el botón atrás.
significado importa
Esta técnica permite la validación de los datos del controlador de vista y el retorno condicional desde el botón de retroceso del controlador de navegación.
gjpc
Esta solución rompe la función de deslizamiento de borde de iOS 7+
Liron Yahdav
9

Esta es la forma correcta de detectar esto.

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        //do stuff

    }
}

este método también se llama cuando se empuja la vista. Entonces, verificar parent == nil es para extraer el controlador de vista de la pila

Saad
fuente
9

Termino con estas soluciones. Cuando tocamos el botón de retroceso viewDidDisappear método llamado. podemos verificar llamando al selector isMovingFromParentViewController que devuelve verdadero. podemos devolver los datos (usando Delegate) .Espero que esto ayude a alguien.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

    }
    if (self.isMovingFromParentViewController) {
       //moving back
        //pass to viewCollection delegate and update UI
        [self.delegateObject passBackSavedData:self.dataModel];

    }
}
Avijit Nagare
fuente
No lo olvides[super viewDidDisappear:animated]
SamB
9

Tal vez sea un poco tarde, pero también quería el mismo comportamiento antes. Y la solución con la que elegí funciona bastante bien en una de las aplicaciones que se encuentran actualmente en la App Store. Como no he visto a nadie usar un método similar, me gustaría compartirlo aquí. La desventaja de esta solución es que requiere subclases UINavigationController. Aunque use el método Swizzling podría ayudar a evitar eso, no fui tan lejos.

Entonces, el botón de retroceso predeterminado es administrado por UINavigationBar. Cuando un usuario toca el botón de retroceso, UINavigationBarpregúntele a su delegado si debería abrir la parte superior UINavigationItemllamando navigationBar(_:shouldPop:). UINavigationControlleren realidad implementa esto, pero no declara públicamente que lo adopta UINavigationBarDelegate(¿por qué?). Para interceptar este evento, cree una subclase de UINavigationController, declare su conformidad UINavigationBarDelegatee implemente navigationBar(_:shouldPop:). Regrese truesi el elemento superior debe aparecer. Regrese falsesi debe quedarse.

Hay dos problemas. La primera es que debes llamar a la UINavigationControllerversión de navigationBar(_:shouldPop:)en algún momento. Pero UINavigationBarControllerno lo declara públicamente de conformidad UINavigationBarDelegate, intentar llamarlo resultará en un error de tiempo de compilación. La solución con la que fui es usar el tiempo de ejecución de Objective-C para obtener la implementación directamente y llamarla. Por favor, avíseme si alguien tiene una mejor solución.

El otro problema es que navigationBar(_:shouldPop:)se llama primero y sigue popViewController(animated:)si el usuario toca el botón Atrás. El orden se invierte si el controlador de vista se abre al llamar popViewController(animated:). En este caso, utilizo un valor booleano para detectar si popViewController(animated:)se llama antes, lo navigationBar(_:shouldPop:)que significa que el usuario ha pulsado el botón Atrás.

Además, hago una extensión de UIViewControllerpara permitir que el controlador de navegación pregunte al controlador de vista si debe aparecer si el usuario toca el botón Atrás. Los controladores de View pueden regresar falsey realizar las acciones necesarias y llamar popViewController(animated:)más tarde.

class InterceptableNavigationController: UINavigationController, UINavigationBarDelegate {
    // If a view controller is popped by tapping on the back button, `navigationBar(_:, shouldPop:)` is called first follows by `popViewController(animated:)`.
    // If it is popped by calling to `popViewController(animated:)`, the order reverses and we need this flag to check that.
    private var didCallPopViewController = false

    override func popViewController(animated: Bool) -> UIViewController? {
        didCallPopViewController = true
        return super.popViewController(animated: animated)
    }

    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // If this is a subsequence call after `popViewController(animated:)`, we should just pop the view controller right away.
        if didCallPopViewController {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        }

        // The following code is called only when the user taps on the back button.

        guard let vc = topViewController, item == vc.navigationItem else {
            return false
        }

        if vc.shouldBePopped(self) {
            return originalImplementationOfNavigationBar(navigationBar, shouldPop: item)
        } else {
            return false
        }
    }

    func navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem) {
        didCallPopViewController = false
    }

    /// Since `UINavigationController` doesn't publicly declare its conformance to `UINavigationBarDelegate`,
    /// trying to called `navigationBar(_:shouldPop:)` will result in a compile error.
    /// So, we'll have to use Objective-C runtime to directly get super's implementation of `navigationBar(_:shouldPop:)` and call it.
    private func originalImplementationOfNavigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        let sel = #selector(UINavigationBarDelegate.navigationBar(_:shouldPop:))
        let imp = class_getMethodImplementation(class_getSuperclass(InterceptableNavigationController.self), sel)
        typealias ShouldPopFunction = @convention(c) (AnyObject, Selector, UINavigationBar, UINavigationItem) -> Bool
        let shouldPop = unsafeBitCast(imp, to: ShouldPopFunction.self)
        return shouldPop(self, sel, navigationBar, item)
    }
}

extension UIViewController {
    @objc func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        return true
    }
}

Y en su vista controladores, implemente shouldBePopped(_:). Si no implementa este método, el comportamiento predeterminado será abrir el controlador de vista tan pronto como el usuario toque el botón de retroceso como de costumbre.

class MyViewController: UIViewController {
    override func shouldBePopped(_ navigationController: UINavigationController) -> Bool {
        let alert = UIAlertController(title: "Do you want to go back?",
                                      message: "Do you really want to go back? Tap on \"Yes\" to go back. Tap on \"No\" to stay on this screen.",
                                      preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: nil))
        alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { _ in
            navigationController.popViewController(animated: true)
        }))
        present(alert, animated: true, completion: nil)
        return false
    }
}

Puedes ver mi demo aquí .

ingrese la descripción de la imagen aquí

yusuke024
fuente
¡Esta es una solución increíble y debería integrarse en una publicación de blog! Parece exagerado para lo que estoy buscando en este momento, pero en otras circunstancias, vale la pena intentarlo.
ASSeeger
6

Para "ANTES de quitar la vista de la pila":

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        NSLog(@"do whatever you want here");
    }
}
Anum Malik
fuente
5

Hay una forma más apropiada que preguntar a viewControllers. Puede hacer que su controlador sea un delegado de la barra de navegación que tiene el botón Atrás. He aquí un ejemplo. En la implementación del controlador donde desea manejar la presión del botón Atrás, dígale que implementará el protocolo UINavigationBarDelegate:

@interface MyViewController () <UINavigationBarDelegate>

Luego, en algún lugar de su código de inicialización (probablemente en viewDidLoad) haga que su controlador sea el delegado de su barra de navegación:

self.navigationController.navigationBar.delegate = self;

Finalmente, implemente el método shouldPopItem. Este método se llama justo cuando se presiona el botón Atrás. Si tiene varios controladores o elementos de navegación en la pila, probablemente querrá comprobar cuál de esos elementos de navegación aparece (el parámetro del elemento), para que solo haga sus cosas personalizadas cuando lo espera. He aquí un ejemplo:

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    NSLog(@"Back button got pressed!");
    //if you return NO, the back button press is cancelled
    return YES;
}
Carlos Guzman
fuente
4
no funcionó para mí .. lástima porque es delgado. "*** Finalizando la aplicación debido a una excepción no detectada 'NSInternalInconsistencyException', motivo: 'No se puede configurar manualmente el delegado en una UINavigationBar administrada por un controlador'"
DynamicDan
Desafortunadamente, esto no funcionará con un UINavigationController, en su lugar, necesita un UIViewController estándar con un UINavigationBar en él. Esto significa que no puede aprovechar varios de los controles automáticos de visualización que empuja y hace estallar que le brinda NavigationController. ¡Lo siento!
Carlos Guzman
Acabo de usar UINavigationBar en lugar de NavigationBarController y luego funciona bien. Sé que la pregunta es sobre NavigationBarController, pero esta solución es ajustada.
appsunited
3

Si no puede utilizar "viewWillDisappear" o un método similar, intente crear una subclase de UINavigationController. Esta es la clase de encabezado:

#import <Foundation/Foundation.h>
@class MyViewController;

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Clase de implementación:

#import "CCNavigationController.h"
#import "MyViewController.h"

@implementation CCNavigationController {

}
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    @"This is the moment for you to do whatever you want"
    [self.viewController doCustomMethod];
    return [super popViewControllerAnimated:animated];
}

@end

Por otro lado, debe vincular este viewController a su NavigationController personalizado, por lo que, en su método viewDidLoad para su viewController habitual, haga esto:

@implementation MyViewController {
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        ((CCNavigationController*)self.navigationController).viewController = self;
    }
}
George Harley
fuente
3

Aquí hay otra forma en que implementé (no la probé con una secuencia de desenrollado, pero probablemente no se diferenciaría, como otros han declarado con respecto a otras soluciones en esta página) para que el controlador de vista principal realice acciones antes que el VC secundario que empujó sale de la pila de vistas (utilicé esto un par de niveles hacia abajo del UINavigationController original). Esto también podría usarse para realizar acciones antes de que se presione el childVC. Esto tiene la ventaja adicional de trabajar con el botón de retroceso del sistema iOS, en lugar de tener que crear un UIBarButtonItem o UIButton personalizado.

  1. Haga que su VC padre adopte el UINavigationControllerDelegateprotocolo y se registre para mensajes delegados:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
  2. Implemente este UINavigationControllerDelegatemétodo de instancia en MyParentViewController:

    - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
        // Test if operation is a pop; can also test for a push (i.e., do something before the ChildVC is pushed
        if (operation == UINavigationControllerOperationPop) {
            // Make sure it's the child class you're looking for
            if ([fromVC isKindOfClass:[ChildViewController class]]) {
                // Can handle logic here or send to another method; can also access all properties of child VC at this time
                return [self didPressBackButtonOnChildViewControllerVC:fromVC];
            }
        }
        // If you don't want to specify a nav controller transition
        return nil;
    }
  3. Si especifica una función de devolución de llamada específica en el UINavigationControllerDelegatemétodo de instancia anterior

    -(id <UIViewControllerAnimatedTransitioning>)didPressBackButtonOnAddSearchRegionsVC:(UIViewController *)fromVC {
        ChildViewController *childVC = ChildViewController.new;
        childVC = (ChildViewController *)fromVC;
    
        // childVC.propertiesIWantToAccess go here
    
        // If you don't want to specify a nav controller transition
        return nil;

    }

Evan R
fuente
1

Esto es lo que me funciona en Swift:

override func viewWillDisappear(_ animated: Bool) {
    if self.navigationController?.viewControllers.index(of: self) == nil {
        // back button pressed or back gesture performed
    }

    super.viewWillDisappear(animated)
}
pableiros
fuente
0

Si está usando un Storyboard y viene de una transición de empuje, también puede simplemente anular shouldPerformSegueWithIdentifier:sender:.

Mojo66
fuente