Détecter lorsque le bouton `` retour '' est enfoncé sur une barre de navigation


135

J'ai besoin d'effectuer certaines actions lorsque le bouton de retour (retour à l'écran précédent, retour à la vue parent) est enfoncé sur une barre de navigation.

Existe-t-il une méthode que je peux mettre en œuvre pour capturer l'événement et déclencher des actions pour mettre en pause et enregistrer les données avant que l'écran ne disparaisse?


duplication possible de l' action
nielsbot


Je l'ai fait de cette façon montrer la décision ici
Taras

Réponses:


316

MISE À JOUR: Selon certains commentaires, la solution de la réponse originale ne semble pas fonctionner dans certains scénarios sous iOS 8+. Je ne peux pas vérifier que c'est effectivement le cas sans plus de détails.

Pour ceux d'entre vous, cependant, dans cette situation, il existe une alternative. Détecter le moment où un contrôleur de vue est en cours de saut est possible par remplacement willMove(toParentViewController:). L'idée de base est qu'un contrôleur de vue est affiché quand parentest nil.

Consultez «Implémentation d'un contrôleur de vue de conteneur» pour plus de détails.


Depuis iOS 5, j'ai trouvé que le moyen le plus simple de gérer cette situation consiste à utiliser la nouvelle méthode - (BOOL)isMovingFromParentViewController:

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

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

- (BOOL)isMovingFromParentViewController est logique lorsque vous poussez et sautez des contrôleurs dans une pile de navigation.

Cependant, si vous présentez des contrôleurs de vue modale, vous devez utiliser à la - (BOOL)isBeingDismissedplace:

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

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

Comme indiqué dans cette question , vous pouvez combiner les deux propriétés:

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

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

D'autres solutions reposent sur l'existence d'un UINavigationBar. Au lieu de cela, j'aime davantage mon approche car elle dissocie les tâches requises à effectuer de l'action qui a déclenché l'événement, c'est-à-dire en appuyant sur un bouton de retour.


J'aime ta réponse. Mais pourquoi avez-vous utilisé «self.isBeingDismissed»? Dans mon cas, les déclarations dans «self.isBeingDismissed» ne sont pas implémentées.
Rutvij Kotecha

3
self.isMovingFromParentViewControllera une valeur TRUE lorsque je fais apparaître la pile de navigation par programmation en utilisant popToRootViewControllerAnimated- sans aucune pression sur le bouton de retour. Dois-je décliner votre réponse? (le sujet dit que "le bouton" retour "est pressé sur une barre de navigation")
kas-kad

2
Excellente réponse, merci beaucoup. Dans Swift j'ai utilisé:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }
Camillo Visini

1
Vous ne devriez le faire qu'à l'intérieur, -viewDidDisappear:car il est possible que vous obteniez un -viewWillDisappear:sans a -viewDidDisappear:(comme lorsque vous commencez à faire glisser pour ignorer un élément du contrôleur de navigation, puis à annuler ce balayage.
Heath Borders

3
Cela ne semble plus être une solution fiable. A travaillé au moment où j'ai utilisé ceci pour la première fois (c'était iOS 10). Mais maintenant, j'ai accidentellement trouvé qu'il cessait de fonctionner calmement (iOS 11). A dû passer à la solution "willMove (toParentViewController)".
Vitalii

100

Alors que viewWillAppear()et viewDidDisappear() sont appelés lorsque le bouton de retour est appuyé, ils sont également appelés à d'autres moments. Voir la fin de la réponse pour en savoir plus.

Utilisation de UIViewController.parent

Il est préférable de détecter le bouton de retour lorsque le VC est supprimé de son parent (le NavigationController) à l'aide de willMoveToParentViewController(_:)OUdidMoveToParentViewController()

Si parent est nul, le contrôleur de vue est sorti de la pile de navigation et rejeté. Si le parent n'est pas nul, il est ajouté à la pile et présenté.

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

Swap sur willMovepour didMoveet le contrôle self.parent pour faire le travail après le contrôleur de vue est rejeté.

Arrêter le licenciement

Notez que la vérification du parent ne vous permet pas de "suspendre" la transition si vous devez faire une sorte de sauvegarde asynchrone. Pour ce faire, vous pouvez implémenter ce qui suit. Le seul inconvénient ici est que vous perdez le bouton de retour de style / animé iOS. Faites également attention ici avec le geste de balayage interactif. Utilisez ce qui suit pour gérer ce cas.

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


Plus sur la vue apparaîtra / est apparu

Si vous n'avez pas rencontré le viewWillAppear viewDidDisappearproblème, passons en revue un exemple. Supposons que vous ayez trois contrôleurs de vue:

  1. ListVC: un tableau des choses
  2. DetailVC: Détails sur une chose
  3. SettingsVC: Quelques options pour une chose

Permet de suivre les appels au detailVCfur et à mesure que vous passez de listVCà settingsVCet de retour àlistVC

Liste> Détail (push detailVC) Detail.viewDidAppear<- apparaît
Detail> Settings (push settingsVC) Detail.viewDidDisappear<- disparaît

Et comme nous revenons en arrière ...
Paramètres> Détail (pop settingsVC) Detail.viewDidAppear<- apparaissent
Détails> Liste (pop detailVC) Detail.viewDidDisappear<- disparaissent

Remarquez qu'il viewDidDisappearest appelé plusieurs fois, non seulement lors du retour, mais également lors de la progression. Pour une opération rapide qui peut être souhaitée, mais pour une opération plus complexe comme un appel réseau à enregistrer, ce n'est peut-être pas le cas.


Juste une note, l'utilisateur didMoveToParantViewController:doit travailler lorsque la vue n'est plus visible. Utile pour iOS7 avec le interactiveGesutre
WCByrne

didMoveToParentViewController * il y a une faute de frappe
thewormsterror

N'oubliez pas d'appeler [super willMoveToParentViewController: parent]!
ScottyB

2
Le paramètre parent est nul lorsque vous accédez au contrôleur de vue parent et non nul lorsque la vue dans laquelle cette méthode apparaît est affichée. Vous pouvez utiliser ce fait pour effectuer une action uniquement lorsque vous appuyez sur le bouton Retour, et non lorsque vous arrivez à la vue. C'était, après tout, la question initiale. :)
Mike

1
Cela est également appelé lors de l'utilisation par programme _ = self.navigationController?.popViewController(animated: true), il n'est donc pas simplement appelé lors d'une pression sur le bouton Retour. Je recherche un appel qui ne fonctionne que lorsque vous appuyez sur Retour.
Ethan Allen

16

Première méthode

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

Deuxième méthode

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

1
La deuxième méthode était la seule qui fonctionnait pour moi. La première méthode a également été appelée lors de la présentation de mon point de vue, ce qui n'était pas acceptable pour mon cas d'utilisation.
marcshilling

10

Ceux qui prétendent que cela ne fonctionne pas se trompent:

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

Cela fonctionne très bien. Alors qu'est-ce qui cause le mythe répandu selon lequel ce n'est pas le cas?

Le problème semble être dû à une implémentation incorrecte d'une méthode différente , à savoir que l'implémentation de a willMove(toParent:)oublié d'appeler super.

Si vous implémentez willMove(toParent:)sans appeler super, alors self.isMovingFromParentsera falseet l'utilisation de viewWillDisappearsemblera échouer. Cela n'a pas échoué; tu l'as cassé.

REMARQUE: Le vrai problème est généralement le deuxième contrôleur de vue détectant que le premier contrôleur de vue a été sauté. S'il vous plaît voir aussi la discussion plus générale ici: Unified UIViewController "est devenu le premier" détection?

EDIT Un commentaire suggère que cela devrait être viewDidDisappearplutôt que viewWillDisappear.


Ce code est exécuté lorsque le bouton de retour est appuyé, mais est également exécuté si le VC est sauté par programme.
biomiker

@biomiker Bien sûr, mais cela serait également vrai pour les autres approches. Popping éclate. La question est de savoir comment détecter un pop lorsque vous n'avez pas pop par programme. Si vous pop par programme, vous savez déjà que vous sautez donc il n'y a rien à détecter.
mat

Oui, cela est vrai pour plusieurs des autres approches et beaucoup d'entre elles ont des commentaires similaires. Je clarifiais juste car c'était une réponse récente avec une réfutation spécifique et j'avais eu mes espoirs quand je l'ai lu. Pour mémoire, la question est de savoir comment détecter une pression sur le bouton de retour. C'est un argument raisonnable de dire que le code qui s'exécutera également dans les situations où le bouton de retour n'est pas enfoncé, sans indiquer si le bouton de retour a été enfoncé ou non, ne résout pas complètement la vraie question, même si peut-être la question aurait pu être plus explicite sur ce point.
biomiker

1
Malheureusement, cela revient truepour le geste de balayage interactif - à partir du bord gauche du contrôleur de vue - même si le balayage ne l'a pas complètement fait. Donc, au lieu de l'enregistrer willDisappear, faites-le en didDisappeartravaux.
badhanganesh le

1
@badhanganesh Merci, réponse modifiée pour inclure cette information.
mat

9

Je joue (ou me bats) avec ce problème depuis deux jours. IMO, la meilleure approche consiste simplement à créer une classe d'extension et un protocole, comme ceci:

@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

Cela fonctionne parce UINavigationControllerque recevra un appel à navigationBar:shouldPopItem:chaque fois qu'un contrôleur de vue est sauté. Là, nous détectons si le dos a été enfoncé ou non (tout autre bouton). La seule chose que vous devez faire est d'implémenter le protocole dans le contrôleur de vue où back est pressé.

N'oubliez pas de faire apparaître manuellement le contrôleur de vue à l'intérieur backButtonPressedSel, si tout va bien.

Si vous avez déjà sous-classé UINavigationViewControlleret implémenté, navigationBar:shouldPopItem:ne vous inquiétez pas, cela n'interférera pas avec cela.

Vous pourriez également être intéressé par la désactivation du geste du dos.

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

1
Cette réponse était presque complète pour moi, sauf que j'ai trouvé que 2 contrôleurs de vue apparaissaient souvent. Renvoyer YES fait appeler la méthode appelante pop, donc appeler pop signifiait également que 2 contrôleurs de vue seraient sautés. Voir cette réponse sur une autre question pour plus de détails (une très bonne réponse qui mérite plus de votes positifs): stackoverflow.com/a/26084150/978083
Jason Ridge

Bon point, ma description n'était pas claire sur ce fait. Le "N'oubliez pas de pop manuellement le contrôleur de vue si tout va bien" c'est seulement pour le cas de retour "NON", sinon le flux est le pop normal.
7ynk3r

1
Pour la branche "else", il est préférable d'appeler super implémentation si vous ne voulez pas gérer le pop vous-même et le laisser renvoyer ce qu'il pense être juste, ce qui est principalement OUI, mais il s'occupe également du pop lui-même et anime correctement le chevron .
Ben Sinclair

9

Cela fonctionne pour moi dans iOS 9.3.x avec Swift:

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

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

Contrairement à d'autres solutions ici, cela ne semble pas se déclencher de manière inattendue.


il est préférable d'utiliser willMove à la place
Eugene Gordin

4

Pour mémoire, je pense que c'est plus ce qu'il cherchait…

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

1
Merci Paul, cette solution est assez simple. Malheureusement, l'icône est différente. Il s'agit de l'icône «rembobinage», pas de l'icône de retour. Peut-être y a-t-il un moyen d'utiliser l'icône de retour ...
Ferran Maylinch

2

Comme purrrminatordit, la réponse elitalonn'est pas tout à fait correcte, car elle your stuffserait exécutée même lors du saut du contrôleur par programme.

La solution que j'ai trouvée jusqu'à présent n'est pas très sympa, mais cela fonctionne pour moi. En plus de ce qui a été elitalondit, je vérifie également si je suis programmé ou non:

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

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

Vous devez ajouter cette propriété à votre contrôleur et la définir sur OUI avant de sauter par programme:

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

Merci de votre aide!


2

La meilleure façon est d'utiliser les méthodes de délégué UINavigationController

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

En utilisant cela, vous pouvez savoir quel contrôleur affiche le UINavigationController.

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

Cela devrait être marqué comme la bonne réponse! Pourrait également vouloir ajouter une ligne de plus juste pour rappeler aux gens -> self.navigationController.delegate = self;
Mike Critchley

2

J'ai résolu ce problème en ajoutant un UIControl à la barre de navigation sur le côté gauche.

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

Et vous devez vous rappeler de le supprimer lorsque la vue disparaîtra:

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

C'est tout!


2

Vous pouvez utiliser le rappel du bouton retour, comme ceci:

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

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

pour la version rapide, vous pouvez faire quelque chose comme dans une portée globale

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

En dessous de celui-ci, vous placez dans le viewcontroller où vous souhaitez contrôler l'action du bouton retour:

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

    return true
}

1
Je ne sais pas pourquoi quelqu'un a voté. Cela semble être de loin la meilleure réponse.
Avinash

@Avinash D'où navigationShouldPopOnBackButtonvient-il? Il ne fait pas partie de l'API publique.
elitalon

@elitalon Désolé, c'était une demi-réponse. J'avais pensé que le reste du contexte était là en question. Quoi qu'il en soit, j'ai mis à jour la réponse maintenant
Avinash

1

Comme l'a dit Coli88, vous devriez vérifier le protocole UINavigationBarDelegate.

De manière plus générale, vous pouvez également utiliser - (void)viewWillDisapear:(BOOL)animatedpour effectuer un travail personnalisé lorsque la vue conservée par le contrôleur de vue actuellement visible est sur le point de disparaître. Malheureusement, cela couvrirait les problèmes de poussée et de pop.


1

Pour Swift avec un UINavigationController:

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

1

La réponse de 7ynk3r était très proche de ce que j'ai utilisé à la fin, mais il fallait quelques ajustements:

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


0

self.navigationController.isMovingFromParentViewController ne fonctionne plus sur iOS8 et 9 j'utilise:

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

-1

(RAPIDE)

solution finalement trouvée .. la méthode que nous recherchions est "willShowViewController" qui est la méthode déléguée 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
        }
    }
}

1
Le problème avec cette approche est qu'il couple MyViewControllerà PushedController.
clozach
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.