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?

ewok
la source
duplication possible de l' action
nielsbot
1
Regardez la solution dans ce fil
Jiri Volejnik
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.

élitalon
la source
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.

WCByrne
la source
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];
}
Zar E Ahmer
la source
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.

mat
la source
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;
}
7ynk3r
la source
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.

Chris Villa
la source
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];
    }
Paul Brady
la source
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!

Ferran Maylinch
la source
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");
}
Harald
la source
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!

Eric
la source
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
}
Pedro Magalhães
la source
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.

Ramdam
la source
1

Pour Swift avec un UINavigationController:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
la source
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;
    }
}
micromanc3r
la source
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
    }
}
Vassily
la source
-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
        }
    }
}
Jiří Zahálka
la source
1
Le problème avec cette approche est qu'il couple MyViewControllerà PushedController.
clozach