Rilevamento quando il pulsante 'indietro' viene premuto su una barra di navigazione


135

Devo eseguire alcune azioni quando il pulsante Indietro (torna alla schermata precedente, torna alla vista padre) viene premuto su una barra di navigazione.

Esiste un metodo che posso implementare per catturare l'evento e lanciare alcune azioni per mettere in pausa e salvare i dati prima che scompaia lo schermo?




Risposte:


316

AGGIORNAMENTO: Secondo alcuni commenti, la soluzione nella risposta originale non sembra funzionare in determinati scenari in iOS 8+. Non posso verificare che sia effettivamente così senza ulteriori dettagli.

Per quelli di voi, tuttavia, in quella situazione c'è un'alternativa. È possibile rilevare quando viene visualizzato un controller di visualizzazione eseguendo l'override willMove(toParentViewController:). L'idea di base è che un controller di visualizzazione viene visualizzato quando lo parentè nil.

Scopri "Implementazione di un controller Container View" per ulteriori dettagli.


Da iOS 5 ho scoperto che il modo più semplice per affrontare questa situazione è usare il nuovo metodo - (BOOL)isMovingFromParentViewController:

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

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

- (BOOL)isMovingFromParentViewController ha senso quando si spinge e fa scoppiare i controller in uno stack di navigazione.

Tuttavia, se si presentano controller di visualizzazione modali, è necessario utilizzare - (BOOL)isBeingDismissedinvece:

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

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

Come notato in questa domanda , è possibile combinare entrambe le proprietà:

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

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

Altre soluzioni si basano sull'esistenza di a UINavigationBar. Piuttosto mi piace di più il mio approccio perché disaccoppia le attività richieste da eseguire dall'azione che ha scatenato l'evento, cioè premendo un pulsante Indietro.


Mi piace la tua risposta. Ma perché hai usato "self.isBeingDismissed"? Nel mio caso, le dichiarazioni in "self.isBeingDismissed" non vengono implementate.
Rutvij Kotecha,

3
self.isMovingFromParentViewControllerha un valore VERO quando sto popolando lo stack di navigazione a livello di codice usando popToRootViewControllerAnimated- senza alcun tocco sul pulsante Indietro. Dovrei sottovalutare la tua risposta? (il soggetto dice "il pulsante 'indietro' è premuto su una barra di navigazione")
kas-kad

2
Ottima risposta, grazie mille. In Swift ho usato:override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController(){ println("back button pressed") } }
Camillo Visini

1
Dovresti farlo solo -viewDidDisappear:perché è possibile che otterrai un -viewWillDisappear:senza -viewDidDisappear:(come quando inizi a scorrere per chiudere un elemento del controller di navigazione e quindi annullare quel colpo.
Heath Borders

3
Non sembra più una soluzione affidabile. Ha funzionato al momento in cui l'ho usato per la prima volta (era iOS 10). Ma ora l'ho trovato accidentalmente smesso di funzionare con calma (iOS 11). Ho dovuto passare alla soluzione "willMove (toParentViewController)".
Vitalii,

100

Mentre viewWillAppear()e viewDidDisappear() vengono chiamati quando si tocca il pulsante Indietro, vengono anche chiamati altre volte. Vedi fine della risposta per ulteriori informazioni al riguardo.

Utilizzando UIViewController.parent

È meglio rilevare il pulsante Indietro quando il VC viene rimosso dal suo genitore (NavigationController) con l'aiuto di willMoveToParentViewController(_:)ORdidMoveToParentViewController()

Se parent è zero, il controller di visualizzazione viene rimosso dallo stack di navigazione e eliminato. Se parent non è zero, viene aggiunto allo stack e presentato.

// 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 fuori willMoveper didMovee controllo self.parent per fare il lavoro dopo il controller della vista è respinto.

Fermare il licenziamento

Nota: il controllo del genitore non ti consente di "mettere in pausa" la transizione se devi eseguire una sorta di salvataggio asincrono. Per fare ciò è possibile implementare quanto segue. L'unico aspetto negativo qui è che si perde il pulsante posteriore in stile iOS animato / animato. Fai anche attenzione qui con il gesto di scorrimento interattivo. Utilizzare quanto segue per gestire questo caso.

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


Ulteriori informazioni verranno visualizzate / visualizzate

Se non hai riscontrato il viewWillAppear viewDidDisappearproblema, analizziamo un esempio. Supponiamo che tu abbia tre controller di visualizzazione:

  1. ListVC: una vista tabella delle cose
  2. DetailVC: Dettagli su una cosa
  3. SettingsVC: alcune opzioni per una cosa

Consente di seguire le chiamate detailVCmentre si procede dal listVCal settingsVCe viceversalistVC

Elenco> Dettagli (push detailVC) Detail.viewDidAppear<- compare
Dettagli> Impostazioni (push settingsVC) Detail.viewDidDisappear<- scompare

E mentre torniamo indietro ...
Impostazioni> Dettagli (pop settingsVC) Detail.viewDidAppear<- Appare
Dettagli> Elenco (pop detailVC) Detail.viewDidDisappear<- scomparire

Si noti che viewDidDisappearviene chiamato più volte, non solo quando si torna indietro, ma anche quando si va avanti. Per un'operazione rapida che può essere desiderata, ma per un'operazione più complessa come una chiamata di rete da salvare, potrebbe non esserlo.


Solo una nota, l'utente didMoveToParantViewController:deve fare il lavoro quando la vista non è più visibile. Utile per iOS7 con InteractiveGesutre
WCByrne

didMoveToParentViewController * c'è un errore di battitura
thewormsterror

Non dimenticare di chiamare [super willMoveToParentViewController: parent]!
ScottyB,

2
Il parametro parent è zero quando si esegue il popping nel controller della vista parent e non-zero quando viene visualizzata la vista in cui viene visualizzato questo metodo. È possibile utilizzare tale fatto per eseguire un'azione solo quando si preme il pulsante Indietro e non quando si arriva alla vista. Dopo tutto, questa era la domanda originale. :)
Mike

1
Questo viene anche chiamato quando si utilizza a livello di codice _ = self.navigationController?.popViewController(animated: true), quindi non si chiama semplicemente premendo il pulsante Indietro. Sto cercando una chiamata che funziona solo quando si preme Indietro.
Ethan Allen,

16

Primo metodo

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

Secondo metodo

-(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
Il secondo metodo è stato l'unico che ha funzionato per me. Il primo metodo è stato anche richiesto dal mio punto di vista presentato, il che non era accettabile per il mio caso d'uso.
marcshilling

10

Coloro che affermano che questo non funziona si sbagliano:

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

Funziona benissimo. Quindi cosa sta causando il mito diffuso che non lo fa?

Il problema sembra essere dovuto a un'implementazione errata di un metodo diverso , ovvero l'implementazione di willMove(toParent:)Forgot to call super.

Se si implementa willMove(toParent:)senza chiamare super, self.isMovingFromParentlo saràfalse e l'uso di viewWillDisappearsembrerà fallire. Non ha fallito; l'hai rotto.

NOTA: il vero problema è in genere il controller della seconda vista che rileva che il controller della prima vista è stato visualizzato. Vedi anche la discussione più generale qui: Unified UIViewController "è diventato il rilevamento più in anticipo"?

MODIFICA Un commento suggerisce che questo dovrebbe essere viewDidDisappearpiuttosto che viewWillDisappear.


Questo codice viene eseguito quando si tocca il pulsante Indietro, ma viene anche eseguito se il VC viene attivato a livello di codice.
biomiker,

@biomiker Certo, ma sarebbe vero anche per gli altri approcci. Popping sta saltando fuori. La domanda è come rilevare un pop quando non lo fai pop programmaticamente. Se si pop programatically che già sapete si stanno spuntando quindi non c'è nulla da rilevare.
matt

Sì, questo è vero per molti degli altri approcci e molti di questi hanno commenti simili. Stavo solo chiarendo poiché questa era una risposta recente con una confutazione specifica e avevo avuto le mie speranze quando l'ho letto. Per la cronaca, tuttavia, la domanda è come rilevare una pressione del pulsante Indietro. È un argomento ragionevole affermare che il codice che verrà eseguito anche in situazioni in cui il pulsante Indietro non viene premuto, senza indicare se il pulsante Indietro è stato premuto, non risolve completamente la vera domanda, anche se forse la domanda avrebbe potuto essere più esplicito su questo punto.
biomiker,

1
Sfortunatamente questo ritorna trueper il gesto di pop-swipe interattivo - dal bordo sinistro del controller di visualizzazione - anche se lo swipe non lo ha pop completamente. Quindi, invece di registrarlo willDisappear, farlo nelle didDisappearopere.
badhanganesh,

1
@badhanganesh Grazie, risposta modificata per includere tali informazioni.
matt

9

Ho giocato (o combattuto) con questo problema per due giorni. IMO l'approccio migliore è solo quello di creare una classe di estensione e un protocollo, come questo:

@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

Questo funziona perché UINavigationControllerriceverà una chiamata anavigationBar:shouldPopItem: ogni volta che viene visualizzato un controller di visualizzazione. Lì rileviamo se è stato premuto o meno (qualsiasi altro pulsante). L'unica cosa che devi fare è implementare il protocollo nel controller di visualizzazione in cui è premuto back.

Ricorda di inserire manualmente il controller di visualizzazione all'interno backButtonPressedSel, se tutto è a posto.

Se hai già una sottoclasse UINavigationViewControllere implementato, navigationBar:shouldPopItem:non ti preoccupare, questo non interferirà con esso.

Potresti anche essere interessato a disabilitare il gesto alla schiena.

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

1
Questa risposta è stata quasi completa per me, tranne per il fatto che ho scoperto che spesso sarebbero spuntati 2 viewcontroller. La restituzione di SÌ fa sì che il metodo di chiamata chiami pop, quindi anche chiamare pop significa che verranno spuntati 2 viewcontroller. Vedere questa risposta su un'altra domanda per ulteriori deets (una risposta molto buona che merita più upvotes): stackoverflow.com/a/26084150/978083
Jason Ridge

Bene, la mia descrizione non era chiara su questo fatto. Il "Ricorda di far apparire manualmente il controller di visualizzazione se tutto è a posto" serve solo per restituire "NO", altrimenti il ​​flusso è il pop normale.
7ynk3r,

1
Per il ramo "else", è meglio chiamare super-implementazione se non vuoi gestire il pop te stesso e lasciarlo restituire qualunque cosa pensi sia giusta, che è principalmente SÌ, ma si occupa anche del pop stesso e anima il chevron in modo corretto .
Ben Sinclair

9

Questo funziona per me in iOS 9.3.x con Swift:

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

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

A differenza di altre soluzioni qui, questo non sembra innescarsi inaspettatamente.


è meglio usare willMove invece
Eugene Gordin,

4

Per la cronaca, penso che questo sia più di ciò che stava cercando ...

    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
Grazie Paolo, questa soluzione è abbastanza semplice. Sfortunatamente, l'icona è diversa. Questa è l'icona "riavvolgimento", non l'icona posteriore. Forse c'è un modo per usare l'icona posteriore ...
Ferran Maylinch,

2

Come purrrminatordetto, la risposta elitalonnon è del tutto corretta, da allorayour stuff verrebbe eseguita anche quando si fa scattare il controller a livello di codice.

La soluzione che ho trovato finora non è molto bella, ma funziona per me. Oltre a ciò elitalonche ho detto, controllo anche se sto saltando programmaticamente o meno:

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

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

Devi aggiungere quella proprietà al tuo controller e impostarla su SÌ prima di far apparire programmaticamente:

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

Grazie per l'aiuto!


2

Il modo migliore è utilizzare i metodi delegati UINavigationController

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

Usando questo puoi sapere quale controller mostra UINavigationController.

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

Questo dovrebbe essere contrassegnato come la risposta corretta! Potrebbe anche voler aggiungere un'altra riga solo per ricordare alla gente -> self.navigationController.delegate = self;
Mike Critchley,

2

Ho risolto questo problema aggiungendo un UIControl alla barra di navigazione sul lato sinistro.

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

E devi ricordare di rimuoverlo quando la vista scomparirà:

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

È tutto!


2

È possibile utilizzare il callback del pulsante Indietro, in questo modo:

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

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

per la versione rapida puoi fare qualcosa come nell'ambito 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
    }
}

Sotto quello inserito nel viewcontroller in cui si desidera controllare l'azione del pulsante Indietro:

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

    return true
}

1
Non so perché qualcuno abbia votato. Questa sembra essere la risposta di gran lunga migliore.
Avinash,

@Avinash Da dove navigationShouldPopOnBackButtonviene? Non fa parte dell'API pubblica.
elitalon

@elitalon Siamo spiacenti, questa è stata la mezza risposta. Pensavo che il contesto rimanente fosse lì in questione. Comunque ho aggiornato la risposta ora
Avinash,

1

Come ha detto Coli88, dovresti controllare il protocollo UINavigationBarDelegate.

In un modo più generale, è anche possibile utilizzare - (void)viewWillDisapear:(BOOL)animatedper eseguire lavori personalizzati quando la vista trattenuta dal controller di vista attualmente visibile sta per scomparire. Sfortunatamente, questo coprirebbe fastidio della spinta e dei casi pop.


1

Per Swift con un UINavigationController:

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

1

La risposta di 7ynk3r era molto simile a quella che ho usato alla fine, ma aveva bisogno di alcune modifiche:

- (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 non funziona più su iOS8 e 9 che utilizzo:

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

-1

(SWIFT)

la soluzione finalmente trovata .. il metodo che stavamo cercando è "willShowViewController" che è il metodo delegato di 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
Il problema di questo approccio è che le coppie MyViewControllera PushedController.
clozach,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.