richiamata pulsante indietro nella navigazione Controller in iOS


102

Ho inserito una vista sul controller di navigazione e quando premo il pulsante Indietro si passa automaticamente alla vista precedente. Voglio fare alcune cose quando si preme il pulsante Indietro prima di rimuovere la vista dallo stack. Qual è la funzione di richiamata del pulsante Indietro?


possibile duplicato dell'azione
Zakaria

Controlla questa [soluzione] [1] che mantiene anche lo stile del pulsante Indietro. [1]: stackoverflow.com/a/29943156/3839641
Sarasranglt

Risposte:


162

La risposta di William Jockusch risolve questo problema con un semplice trucco.

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

32
Questo codice non viene eseguito solo quando l'utente tocca il pulsante Indietro, ma in ogni caso viene visualizzata la vista (ad esempio, quando si ha un pulsante Fine o Salva sul lato destro).
significato conta

7
O quando si passa a una nuova visualizzazione.
GuybrushThreepwood

Viene chiamato anche quando l'utente esegue una panoramica dal bordo sinistro (interactivePopGestureRecognizer). Nel mio caso, cerco specificamente quando l'utente preme indietro mentre NON esegue la panoramica dal bordo sinistro.
Kyle Clegg

2
Non significa che la causa sia stata il pulsante Indietro. Potrebbe essere un seguito per rilassarsi, ad esempio.
smileBot

1
Ho un dubbio, perché non dovremmo farlo in viewDidDisappear?
JohnVanDijk

85

Secondo me la soluzione migliore.

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

Ma funziona solo con iOS5 +


3
Questa tecnica non è in grado di distinguere tra un tocco del pulsante Indietro e un segue per rilassarsi.
smileBot

Il metodo willMoveToParentViewController e viewWillDisappear non spiega che il controller deve essere distrutto, didMoveToParentViewController ha ragione
Hank

27

probabilmente è meglio sovrascrivere il pulsante Indietro in modo da poter gestire l'evento prima che la visualizzazione venga visualizzata per cose come la conferma dell'utente.

in viewDidLoad crea un UIBarButtonItem e imposta self.navigationItem.leftBarButtonItem passandogli 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];

}

Quindi puoi fare cose come sollevare un UIAlertView per confermare l'azione, quindi aprire il controller della vista, ecc.

Oppure, invece di creare un nuovo pulsante Indietro, puoi conformarti ai metodi delegati UINavigationController per eseguire azioni quando viene premuto il pulsante Indietro.


Il UINavigationControllerDelegatenon ha metodi che vengono chiamati quando si tocca il pulsante Indietro.
significato conta

Questa tecnica consente la convalida dei dati del controller di visualizzazione e il ritorno condizionale dal pulsante Indietro del controller di navigazione.
gjpc

Questa soluzione interrompe la funzione di scorrimento
laterale

9

Questo è il modo corretto per rilevarlo.

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

    }
}

questo metodo viene chiamato anche quando viene eseguito il push della visualizzazione. Quindi controllare genitore == nil serve per estrarre il controller di visualizzazione dallo stack


9

Finisco con queste soluzioni. Mentre tocchiamo indietro il metodo viewDidDisappear chiamato. possiamo controllare chiamando il selettore isMovingFromParentViewController che restituisce true. possiamo restituire i dati (usando Delegate). spero che questo aiuti qualcuno.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

    }
    if (self.isMovingFromParentViewController) {
       //moving back
        //pass to viewCollection delegate and update UI
        [self.delegateObject passBackSavedData:self.dataModel];

    }
}

Non dimenticare[super viewDidDisappear:animated]
SamB

9

Forse è un po 'troppo tardi, ma volevo anche lo stesso comportamento prima. E la soluzione che ho scelto funziona abbastanza bene in una delle app attualmente sull'App Store. Dato che non ho visto nessuno con un metodo simile, vorrei condividerlo qui. Lo svantaggio di questa soluzione è che richiede la creazione di sottoclassi UINavigationController. Sebbene l'utilizzo del Metodo Swizzling possa aiutare a evitarlo, non sono andato così lontano.

Quindi, il pulsante Indietro predefinito è effettivamente gestito da UINavigationBar. Quando un utente tocca il pulsante Indietro, UINavigationBarchiedi al suo delegato se deve apparire in alto UINavigationItemchiamando navigationBar(_:shouldPop:). UINavigationControllerlo implementa effettivamente, ma non dichiara pubblicamente di adottare UINavigationBarDelegate(perché !?). Per intercettare questo evento, creare una sottoclasse di UINavigationController, dichiararne la conformità UINavigationBarDelegatee implementare navigationBar(_:shouldPop:). Restituisci truese l'elemento in alto deve essere estratto. Ritorna falsese dovrebbe restare.

Ci sono due problemi. Il primo è che a un certo punto devi chiamare la UINavigationControllerversione di navigationBar(_:shouldPop:). Ma UINavigationBarControllernon lo dichiara pubblicamente conforme a UINavigationBarDelegate, provare a chiamarlo risulterà in un errore in fase di compilazione. La soluzione che ho scelto è usare il runtime Objective-C per ottenere direttamente l'implementazione e chiamarla. Per favore fatemi sapere se qualcuno ha una soluzione migliore.

L'altro problema è che navigationBar(_:shouldPop:)viene chiamato prima popViewController(animated:)se l'utente tocca il pulsante Indietro. L'ordine si inverte se il controller di visualizzazione viene estratto chiamando popViewController(animated:). In questo caso, utilizzo un valore booleano per rilevare se popViewController(animated:)viene chiamato prima, il navigationBar(_:shouldPop:)che significa che l'utente ha toccato il pulsante Indietro.

Inoltre, creo un'estensione di UIViewControllerper consentire al controller di navigazione di chiedere al controller di visualizzazione se deve essere visualizzato se l'utente tocca il pulsante Indietro. I controller di visualizzazione possono tornare falseed eseguire tutte le azioni necessarie e chiamare popViewController(animated:)più tardi.

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

E nella visualizzazione dei controller, implementare shouldBePopped(_:). Se non si implementa questo metodo, il comportamento predefinito sarà quello di far apparire il controller della vista non appena l'utente tocca il pulsante Indietro come al solito.

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

Puoi guardare la mia demo qui .

inserisci qui la descrizione dell'immagine


Questa è una soluzione fantastica e dovrebbe essere inserita in un post sul blog! Sembra essere eccessivo per quello che sto cercando in questo momento, ma in altre circostanze, vale sicuramente la pena provarlo.
ASSeeger

6

Per "PRIMA di rimuovere la visualizzazione dallo stack":

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

5

C'è un modo più appropriato che chiedere ai viewControllers. È possibile rendere il controller un delegato della navigationBar con il pulsante Indietro. Ecco un esempio. Nell'implementazione del controller in cui vuoi gestire la pressione del pulsante Indietro, digli che implementerà il protocollo UINavigationBarDelegate:

@interface MyViewController () <UINavigationBarDelegate>

Quindi da qualche parte nel codice di inizializzazione (probabilmente in viewDidLoad) rendi il tuo controller il delegato della sua barra di navigazione:

self.navigationController.navigationBar.delegate = self;

Infine, implementa il metodo shouldPopItem. Questo metodo viene chiamato proprio quando viene premuto il pulsante Indietro. Se hai più controller o elementi di navigazione nello stack, probabilmente vorrai controllare quale di questi elementi di navigazione viene visualizzato (il parametro dell'elemento), in modo da eseguire le tue cose personalizzate solo quando te lo aspetti. Ecco un esempio:

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    NSLog(@"Back button got pressed!");
    //if you return NO, the back button press is cancelled
    return YES;
}

4
non ha funzionato per me .. peccato perché è magro. "*** Chiusura dell'app a causa di un'eccezione non rilevata" NSInternalInconsistencyException ", motivo:" Impossibile impostare manualmente il delegato su una UINavigationBar gestita da un controller. ""
DynamicDan

Questo purtroppo non funzionerà con un UINavigationController, invece, è necessario un UIViewController standard con una UINavigationBar al suo interno. Ciò significa che non puoi sfruttare molti dei controlli automatici del viewcontroller che ti danno il NavigationController. Scusa!
Carlos Guzman

Ho appena usato UINavigationBar invece di NavigationBarController e poi funziona bene. So che la domanda riguarda il NavigationBarController, ma questa soluzione è snella.
appsunited

3

Se non puoi utilizzare "viewWillDisappear" o un metodo simile, prova a creare una sottoclasse UINavigationController. Questa è la classe di intestazione:

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

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Classe di implementazione:

#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'altra parte, devi collegare questo viewController al tuo NavigationController personalizzato, quindi, nel tuo metodo viewDidLoad per il tuo viewController normale, fai questo:

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

3

Ecco un altro modo in cui ho implementato (non l'ho testato con un segue di sblocco ma probabilmente non avrebbe differenziato, come altri hanno affermato in merito ad altre soluzioni in questa pagina) per fare in modo che il controller della vista genitore esegua azioni prima che il VC figlio che ha spinto viene saltato fuori dallo stack di visualizzazione (l'ho usato un paio di livelli più in basso rispetto all'originale UINavigationController). Questo potrebbe anche essere usato per eseguire azioni prima che anche il ChildVC venga spinto. Questo ha l'ulteriore vantaggio di lavorare con il pulsante Indietro del sistema iOS, invece di dover creare un UIBarButtonItem o un UIButton personalizzato.

  1. Chiedi al tuo VC genitore di adottare il UINavigationControllerDelegateprotocollo e di registrarsi per i messaggi dei delegati:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
  2. Implementa questo UINavigationControllerDelegatemetodo di istanza in 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. Se specifichi una funzione di callback specifica nel UINavigationControllerDelegatemetodo di istanza precedente

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

    }


1

Questo è ciò che funziona per me in Swift:

override func viewWillDisappear(_ animated: Bool) {
    if self.navigationController?.viewControllers.index(of: self) == nil {
        // back button pressed or back gesture performed
    }

    super.viewWillDisappear(animated)
}

0

Se stai usando uno Storyboard e provieni da un segue push, potresti anche semplicemente sovrascrivere shouldPerformSegueWithIdentifier:sender:.

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.