web-dev-qa-db-fra.com

Détecter lorsque le bouton 'Précédent' est enfoncé sur une barre de navigation

Je dois effectuer certaines actions lorsque le bouton Précédent (revenir à l'écran précédent, revenir à la vue parent) est activé sur une barre de navigation.

Existe-t-il une méthode que je peux mettre en œuvre pour intercepter l'événement et déclencher certaines actions pour mettre en pause et enregistrer les données avant la disparition de l'écran?

120
user440096

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

Pour ceux d'entre vous cependant dans cette situation, il existe une alternative. Il est possible de détecter le déclenchement d'un contrôleur de vue en surchargeant willMove(toParentViewController:). L'idée de base est qu'un contrôleur de vue est affiché lorsque parent est nil.

Départ "Implémentation d'un contrôleur de vue de conteneur" pour plus de détails.


Depuis iOS 5, 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 éjectez des contrôleurs dans une pile de navigation.

Toutefois, si vous présentez des contrôleurs de vue modale, utilisez plutôt - (BOOL)isBeingDismissed:

- (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. C’est plutôt mon approche qui différencie les tâches à exécuter de l’action qui a déclenché l’événement, c’est-à-dire qui appuie sur un bouton Précédent.

301
elitalon

Bien que viewWillAppear() et viewDidDisappear() soient appelés lorsque vous appuyez sur le bouton Précédent, ils sont également appelés à d'autres moments. . Voir la fin de la réponse pour plus d'informations à ce sujet.

Utilisation de UIViewController.parent

La détection du bouton Précédent est plus efficace lorsque le VC est supprimé de son parent (le NavigationController) à l’aide de willMoveToParentViewController(_:) _ OR didMoveToParentViewController()

Si parent est nil, le contrôleur de vue est extrait de la pile de navigation et supprimé. Si parent n'est pas nil, 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(toParentViewController parent: UIViewController?) {
    super.willMove(toParentViewController:parent)
    if parent == nil {
        // The back button was pressed or interactive gesture used
    }
}

Remplacez willMove par didMove et vérifiez que self.parent fonctionne après le contrôleur de vue est ignoré.

Arrêter le licenciement

Notez que vérifier le parent ne vous permet pas de "suspendre" la transition si vous devez effectuer une sorte de sauvegarde asynchrone. Pour ce faire, vous pouvez implémenter ce qui suit. Le seul inconvénient est que vous perdez le bouton arrière stylé/animé pour 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 à la vue apparaîtra/est apparu

Si vous n'avez pas eu le problème viewWillAppearviewDidDisappear, voyons un exemple. Disons que vous avez trois contrôleurs de vue:

  1. ListVC: Une vue sous forme de tableau
  2. DetailVC: Détails sur une chose
  3. SettingsVC: Quelques options pour une chose

Suivons les appels sur le detailVC à mesure que vous passez de listVC à settingsVC et revenez à listVC

Liste> Détails (Push detailVC) Detail.viewDidAppear <- apparaît
Détail> Paramètres (Paramètres Push VC) Detail.viewDidDisappear <- disparaître

Et comme nous revenons en arrière ...
Paramètres> Détails (paramètres contextuels VC) Detail.viewDidAppear <- apparaît
Détail> Liste (pop detailVC) Detail.viewDidDisappear <- disparaître

Notez que viewDidDisappear est appelé plusieurs fois, non seulement lors des retours, mais aussi lors des prochaines étapes. Ce n'est peut-être pas le cas pour une opération rapide, mais pas pour une opération plus complexe, telle qu'un appel réseau à enregistrer.

92
WCByrne

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];
}
16
Xar E Ahmer

J'ai joué (ou combattu) avec ce problème pendant deux jours. La meilleure approche à l’OMI 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 car UINavigationController recevra un appel à navigationBar:shouldPopItem: chaque fois qu'un contrôleur de vue est affiché. Là nous détectons si le dos a été appuyé ou pas (n'importe quel autre bouton). La seule chose à faire est d'implémenter le protocole dans le contrôleur de vue sur lequel vous appuyez sur la touche Retour.

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

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

Vous pouvez également être intéressé par désactiver le geste du dos.

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

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 aux autres solutions proposées ici, cela ne semble pas se déclencher de manière inattendue.

7
Chris Villa

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 bien. Alors, qu'est-ce qui cause le mythe répandu que 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 willMove(toParent:) a oublié d'appeler super.

Si vous implémentez willMove(toParent:) sans appeler super, alors self.isMovingFromParent Sera false et l'utilisation de viewWillDisappear semblera échouer. Cela n'a pas échoué tu l'as cassé.

6
matt

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];
    }
4
Paul Brady

Comme purrrminator dit, la réponse de elitalon n’est pas tout à fait juste, puisque your stuff serait exécuté même lors du démarrage programmé du contrôleur.

La solution que j'ai trouvée jusqu'à présent n'est pas très gentille, mais ça marche pour moi. Outre ce que elitalon a dit, je vérifie également si j'utilise le programme par programmation 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 lui attribuer la valeur YES avant d'afficher le programme par programmation:

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

Merci de votre aide!

2
Ferran Maylinch

Le meilleur moyen consiste à utiliser les méthodes de délégation 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");
}
2
Harald

Pour Swift avec un UINavigationController:

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

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

De manière plus générale, vous pouvez également utiliser la fonction - (void)viewWillDisapear:(BOOL)animated pour 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 affaires Push et Pop.

1
ramdam

J'ai résolu ce problème en ajoutant un UIControl à la barre de navigation située à 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!

1
Eric

Vous devriez vérifier INavigationBarDelegate Protocol . Dans ce cas, vous pouvez utiliser la méthode navigationBar: shouldPopItem:.

1
Coli88

La réponse de 7ynk3r était très proche de ce que j'avais 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;
    }
}
1
micromanc3r

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

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

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

pour Swift vous pouvez faire quelque chose comme dans la 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 que vous placez dans le contrôleur de vue où vous souhaitez contrôler l'action du bouton Précédent:

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

    return true
}
0
Pedro Magalhães

self.navigationController.isMovingFromParentViewController ne fonctionne plus sous iOS8 et 9 J'utilise:

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