rappel du bouton retour dans la navigationContrôleur sous iOS

102

J'ai poussé une vue sur le contrôleur de navigation et lorsque j'appuie sur le bouton de retour, elle revient automatiquement à la vue précédente. Je veux faire quelques choses lorsque le bouton de retour est enfoncé avant de faire sortir la vue de la pile. Quelle est la fonction de rappel du bouton retour?

Namratha
la source
duplication possible de l' action
Zakaria
Découvrez cette [solution] [1] qui conserve également le style du bouton de retour. [1]: stackoverflow.com/a/29943156/3839641
Sarasranglt

Réponses:

162

La réponse de William Jockusch résout ce problème avec une astuce facile.

-(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
la source
32
Ce code n'est pas seulement exécuté lorsque l'utilisateur appuie sur le bouton de retour, mais dans tous les cas, la vue apparaît (par exemple, lorsque vous avez un bouton Terminé ou Enregistrer sur le côté droit).
sens-questions
7
Ou lorsque vous passez à une nouvelle vue.
GuybrushThreepwood
Ceci est également appelé lorsque l'utilisateur effectue un panoramique à partir du bord gauche (interactivePopGestureRecognizer). Dans mon cas, je recherche spécifiquement le moment où l'utilisateur appuie en arrière sans effectuer de panoramique à partir du bord gauche.
Kyle Clegg
2
Cela ne veut pas dire que le bouton de retour en était la cause. Pourrait être un segue de déroulement par exemple.
smileBot
1
J'ai un doute, pourquoi ne devrions-nous pas faire cela dans viewDidDisappear?
JohnVanDijk
85

À mon avis, la meilleure solution.

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

Mais cela ne fonctionne qu'avec iOS5 +

Vide
la source
3
Cette technique ne peut pas faire la distinction entre une touche de retour et une séquence de déroulement.
smileBot
La méthode willMoveToParentViewController et viewWillDisappear n'explique pas que le contrôleur doit être détruit, didMoveToParentViewController a raison
Hank
27

il est probablement préférable de remplacer le bouton arrière pour pouvoir gérer l'événement avant que la vue ne s'affiche pour des choses telles que la confirmation de l'utilisateur.

dans viewDidLoad, créez un UIBarButtonItem et définissez self.navigationItem.leftBarButtonItem en lui passant 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];

}

Ensuite, vous pouvez faire des choses comme élever un UIAlertView pour confirmer l'action, puis afficher le contrôleur de vue, etc.

Ou au lieu de créer un nouveau bouton arrière, vous pouvez vous conformer aux méthodes de délégué UINavigationController pour effectuer des actions lorsque le bouton Précédent est enfoncé.

Roocell
la source
Le UINavigationControllerDelegaten'a pas de méthodes qui sont appelées lorsque le bouton retour est appuyé.
sens-questions
Cette technique permet la validation des données du contrôleur de vue et le retour conditionnel à partir du bouton retour du contrôleur de navigation.
gjpc
Cette solution brise la fonction de balayage des bords d'iOS 7+
Liron Yahdav
9

C'est la bonne façon de détecter cela.

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

    }
}

cette méthode est également appelée lorsque la vue est poussée. Donc, vérifier le parent == nil est pour faire sauter le contrôleur de vue de la pile

Saad
la source
9

Je me retrouve avec ces solutions. Lorsque nous tapons sur le bouton retour, la méthode viewDidDisappear est appelée. nous pouvons vérifier en appelant le sélecteur isMovingFromParentViewController qui renvoie true. nous pouvons renvoyer les données (en utilisant le délégué). J'espère que cela aidera quelqu'un.

-(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
la source
N'oubliez pas[super viewDidDisappear:animated]
SamB
9

C'est peut-être un peu trop tard, mais je voulais aussi le même comportement avant. Et la solution que j'ai choisie fonctionne assez bien dans l'une des applications actuellement sur l'App Store. Comme je n'ai vu personne utiliser une méthode similaire, j'aimerais la partager ici. L'inconvénient de cette solution est qu'elle nécessite un sous-classement UINavigationController. Bien que l'utilisation de Method Swizzling puisse aider à éviter cela, je ne suis pas allé aussi loin.

Ainsi, le bouton de retour par défaut est en fait géré par UINavigationBar. Lorsqu'un utilisateur appuie sur le bouton de retour, UINavigationBardemandez à son délégué s'il doit apparaître en haut UINavigationItemen appelant navigationBar(_:shouldPop:). UINavigationControllerimplémente réellement ceci, mais il ne déclare pas publiquement qu'il adopte UINavigationBarDelegate(pourquoi!?). Pour intercepter cet événement, créez une sous-classe de UINavigationController, déclarez sa conformité UINavigationBarDelegateet implémentez navigationBar(_:shouldPop:). Renvoyez truesi l'élément supérieur doit être sauté. Revenez falses'il doit rester.

Il y a deux problèmes. La première est que vous devez appeler la UINavigationControllerversion de navigationBar(_:shouldPop:)à un moment donné. Mais UINavigationBarControllerne le déclare pas publiquement conforme à UINavigationBarDelegate, essayer de l'appeler entraînera une erreur de compilation. La solution que j'ai choisie est d'utiliser le runtime Objective-C pour obtenir directement l'implémentation et l'appeler. Veuillez me faire savoir si quelqu'un a une meilleure solution.

L'autre problème est qu'il navigationBar(_:shouldPop:)est appelé en premier après popViewController(animated:)si l'utilisateur appuie sur le bouton de retour. L'ordre s'inverse si le contrôleur de vue est sauté en appelant popViewController(animated:). Dans ce cas, j'utilise un booléen pour détecter si popViewController(animated:)est appelé avant, navigationBar(_:shouldPop:)ce qui signifie que l'utilisateur a tapé sur le bouton de retour.

De plus, je crée une extension UIViewControllerpour permettre au contrôleur de navigation de demander au contrôleur de vue s'il doit être affiché si l'utilisateur appuie sur le bouton de retour. Les contrôleurs de vue peuvent revenir falseet effectuer toutes les actions nécessaires et appeler popViewController(animated:)plus tard.

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

Et dans vous regardez les contrôleurs, implémentez shouldBePopped(_:). Si vous n'implémentez pas cette méthode, le comportement par défaut sera de faire apparaître le contrôleur de vue dès que l'utilisateur appuie sur le bouton de retour comme d'habitude.

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

Vous pouvez regarder ma démo ici .

entrez la description de l'image ici

yusuke024
la source
C'est une solution géniale et devrait être intégrée à un article de blog! Cela semble exagéré pour ce que je recherche en ce moment, mais dans d'autres circonstances, cela vaut vraiment la peine d'essayer.
ASSeeger
6

Pour "AVANT de sortir la vue de la pile":

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

Il existe un moyen plus approprié que de demander aux viewControllers. Vous pouvez faire de votre contrôleur un délégué de la navigationBar qui a le bouton retour. Voici un exemple. Dans l'implémentation du contrôleur où vous souhaitez gérer la pression sur le bouton retour, dites-lui qu'il implémentera le protocole UINavigationBarDelegate:

@interface MyViewController () <UINavigationBarDelegate>

Puis quelque part dans votre code d'initialisation (probablement dans viewDidLoad), faites de votre contrôleur le délégué de sa barre de navigation:

self.navigationController.navigationBar.delegate = self;

Enfin, implémentez la méthode shouldPopItem. Cette méthode est appelée dès que le bouton retour est enfoncé. Si vous avez plusieurs contrôleurs ou éléments de navigation dans la pile, vous voudrez probablement vérifier lequel de ces éléments de navigation est affiché (le paramètre d'élément), afin de ne faire vos tâches personnalisées que lorsque vous le souhaitez. Voici un exemple:

-(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
la source
4
n'a pas fonctionné pour moi ... dommage parce que c'est maigre. "*** Arrêt de l'application en raison d'une exception non interceptée 'NSInternalInconsistencyException', raison: 'Impossible de définir manuellement le délégué sur un UINavigationBar géré par un contrôleur.'"
DynamicDan
Cela ne fonctionnera malheureusement pas avec un UINavigationController, à la place, vous avez besoin d'un UIViewController standard avec un UINavigationBar. Cela signifie que vous ne pouvez pas profiter de plusieurs des poussées et sauts automatiques du viewcontroller que le NavigationController vous donne. Désolé!
Carlos Guzman
Je viens d'utiliser UINavigationBar au lieu de NavigationBarController et cela fonctionne bien. Je sais que la question concerne le NavigationBarController, mais cette solution est légère.
appsunited
3

Si vous ne pouvez pas utiliser "viewWillDisappear" ou une méthode similaire, essayez de sous-classer UINavigationController. Voici la classe d'en-tête:

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

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Classe d'implémentation:

#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

D'autre part, vous devez lier ce viewController à votre NavigationController personnalisé, donc, dans votre méthode viewDidLoad pour votre viewController régulier, procédez comme suit:

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

Voici une autre façon que j'ai implémentée (je ne l'ai pas testé avec une séquence de déroulement mais cela ne se différencierait probablement pas, comme d'autres l'ont indiqué en ce qui concerne d'autres solutions sur cette page) pour que le contrôleur de vue parent effectue des actions avant que le VC enfant qu'il ne pousse est sorti de la pile de vues (je l'ai utilisé quelques niveaux en dessous de l'UINavigationController d'origine). Cela pourrait également être utilisé pour effectuer des actions avant que le childVC ne soit poussé. Cela a l'avantage supplémentaire de travailler avec le bouton de retour du système iOS, au lieu d'avoir à créer un UIBarButtonItem ou UIButton personnalisé.

  1. Demandez à votre VC parent d'adopter le UINavigationControllerDelegateprotocole et de vous inscrire aux messages des délégués:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
  2. Implémentez cette UINavigationControllerDelegateméthode d'instance dans 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 vous spécifiez une fonction de rappel spécifique dans la UINavigationControllerDelegateméthode d'instance ci-dessus

    -(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
la source
1

Voici ce que cela fonctionne pour moi dans 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
la source
0

Si vous utilisez un Storyboard et que vous venez d'un push segue, vous pouvez également simplement remplacer shouldPerformSegueWithIdentifier:sender:.

Mojo66
la source