Rückruf der Zurück-Taste im Navigationscontroller unter iOS


102

Ich habe eine Ansicht auf den Navigationscontroller verschoben und wenn ich die Zurück-Taste drücke, wird automatisch zur vorherigen Ansicht gewechselt. Ich möchte ein paar Dinge tun, wenn die Zurück-Taste gedrückt wird, bevor die Ansicht vom Stapel genommen wird. Welches ist die Rückruffunktion der Zurück-Taste?



Kasse diese [Lösung] [1], die auch den Stil der Zurück-Schaltfläche beibehält. [1]: stackoverflow.com/a/29943156/3839641
Sarasranglt

Antworten:


162

Die Antwort von William Jockusch löst dieses Problem mit einem einfachen Trick.

-(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
Dieser Code wird nicht nur ausgeführt, wenn der Benutzer auf die Schaltfläche "Zurück" tippt, sondern in jedem Fall wird die Ansicht geöffnet (z. B. wenn rechts eine Schaltfläche "Fertig" oder "Speichern" angezeigt wird).
Bedeutung-Angelegenheiten

7
Oder wenn Sie zu einer neuen Ansicht wechseln.
GuybrushThreepwood

Dies wird auch aufgerufen, wenn der Benutzer vom linken Rand aus schwenkt (InteractivePopGestureRecognizer). In meinem Fall suche ich speziell, wenn der Benutzer zurückdrückt, während er NICHT vom linken Rand aus schwenkt.
Kyle Clegg

2
Das heißt nicht, dass der Zurück-Button die Ursache war. Könnte zum Beispiel ein Abwicklungsabschnitt sein.
smileBot

1
Ich habe Zweifel, warum sollten wir das nicht in viewDidDisappear tun?
JohnVanDijk

85

Meiner Meinung nach die beste Lösung.

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

Es funktioniert aber nur mit iOS5 +


3
Diese Technik kann nicht zwischen einem Zurück-Tippen und einem Abwickeln unterscheiden.
smileBot

Die Methode willMoveToParentViewController und viewWillDisappear erklärt nicht, dass der Controller zerstört werden muss. DidMoveToParentViewController ist richtig
Hank

27

Es ist wahrscheinlich besser, den Backbutton zu überschreiben, damit Sie das Ereignis behandeln können, bevor die Ansicht für Dinge wie die Benutzerbestätigung geöffnet wird.

Erstellen Sie in viewDidLoad ein UIBarButtonItem und setzen Sie self.navigationItem.leftBarButtonItem so, dass es in einem sel übergeben wird

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

}

Anschließend können Sie beispielsweise eine UIAlertView auslösen, um die Aktion zu bestätigen, und dann den Ansichts-Controller öffnen usw.

Anstatt einen neuen Backbutton zu erstellen, können Sie sich an die UINavigationController-Delegatmethoden anpassen, um Aktionen auszuführen, wenn der Back-Button gedrückt wird.


Das UINavigationControllerDelegatehat keine Methoden, die aufgerufen werden, wenn die Zurück-Taste gedrückt wird.
Bedeutung-Angelegenheiten

Diese Technik ermöglicht die Validierung der Daten des Ansichtscontrollers und die bedingte Rückgabe über die Zurück-Schaltfläche des Navigationscontrollers.
gjpc

Diese Lösung bricht die Edge-Swipe-Funktion von iOS 7+
Liron Yahdav

9

Dies ist der richtige Weg, um dies zu erkennen.

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

    }
}

Diese Methode wird aufgerufen, wenn die Ansicht ebenfalls verschoben wird. Wenn Sie also parent == nil überprüfen, wird der View Controller vom Stapel entfernt


9

Am Ende habe ich diese Lösungen. Während wir auf die Schaltfläche "Zurück" tippen, wird die Methode viewDidDisappear aufgerufen. Wir können dies überprüfen, indem wir den isMovingFromParentViewController-Selektor aufrufen, der true zurückgibt. Wir können Daten zurückgeben (mit Delegate). Ich hoffe, dies hilft jemandem.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

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

    }
}

Vergiss nicht[super viewDidDisappear:animated]
SamB

9

Vielleicht ist es etwas zu spät, aber ich wollte das gleiche Verhalten auch schon früher. Und die Lösung, mit der ich mich entschieden habe, funktioniert in einer der Apps, die derzeit im App Store erhältlich sind, recht gut. Da ich noch niemanden gesehen habe, der mit einer ähnlichen Methode arbeitet, möchte ich sie hier teilen. Der Nachteil dieser Lösung ist, dass eine Unterklasse erforderlich ist UINavigationController. Obwohl die Verwendung von Method Swizzling helfen könnte, dies zu vermeiden, bin ich nicht so weit gegangen.

Die Standard-Zurück-Schaltfläche wird also tatsächlich von verwaltet UINavigationBar. Wenn ein Benutzer auf die Schaltfläche "Zurück" tippt, UINavigationBarfragen Sie seinen Delegierten, ob er UINavigationItembei einem Anruf die Spitze öffnen soll navigationBar(_:shouldPop:). UINavigationControllerimplementieren Sie dies tatsächlich, aber es erklärt nicht öffentlich, dass es annimmt UINavigationBarDelegate(warum!?). Um dieses Ereignis abzufangen, erstellen Sie eine Unterklasse von UINavigationController, deklarieren Sie deren Konformität mit UINavigationBarDelegateund implementieren Sie sie navigationBar(_:shouldPop:). Geben Sie zurück, truewenn der oberste Gegenstand geknallt werden soll. Rückkehrfalse wenn es bleiben soll.

Es gibt zwei Probleme. Das erste ist, dass Sie die UINavigationControllerVersion von navigationBar(_:shouldPop:)irgendwann aufrufen müssen . Erklärt es UINavigationBarControlleraber nicht öffentlich als konformUINavigationBarDelegate , führt der Versuch, es aufzurufen, zu einem Fehler bei der Kompilierung. Die Lösung, die ich gewählt habe, besteht darin, die Objective-C-Laufzeit zu verwenden, um die Implementierung direkt abzurufen und aufzurufen. Bitte lassen Sie mich wissen, wenn jemand eine bessere Lösung hat.

Das andere Problem ist, dass navigationBar(_:shouldPop:)zuerst aufgerufen wird, popViewController(animated:)wenn der Benutzer auf die Zurück-Schaltfläche tippt. Die Reihenfolge wird umgekehrt, wenn der View Controller durch Aufrufen geöffnet wird popViewController(animated:). In diesem Fall verwende ich einen Booleschen Wert, um festzustellen, ob popViewController(animated:)zuvor aufgerufen wurde, navigationBar(_:shouldPop:)was bedeutet, dass der Benutzer auf die Schaltfläche "Zurück" getippt hat.

Außerdem mache ich eine Erweiterung von UIViewController, damit der Navigations-Controller den Ansichts-Controller fragt, ob er geöffnet werden soll, wenn der Benutzer auf die Zurück-Schaltfläche tippt. View Controller können zurückkehren falseund alle erforderlichen Aktionen ausführen und popViewController(animated:)später aufrufen .

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

Und in Ihren View-Controllern implementieren shouldBePopped(_:). Wenn Sie diese Methode nicht implementieren, wird standardmäßig der Ansichts-Controller geöffnet, sobald der Benutzer wie gewohnt auf die Schaltfläche "Zurück" tippt.

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

Sie können meine Demo hier ansehen .

Geben Sie hier die Bildbeschreibung ein


Dies ist eine großartige Lösung und sollte in einen Blogpost gebacken werden! Scheint übertrieben für das zu sein, wonach ich gerade suche, aber unter anderen Umständen ist es sicher einen Versuch wert.
ASSeeger

6

Für "BEVOR Sie die Ansicht vom Stapel nehmen":

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

5

Es gibt einen angemesseneren Weg, als die viewController zu fragen. Sie können Ihren Controller zu einem Delegaten der Navigationsleiste machen, die über die Schaltfläche "Zurück" verfügt. Hier ist ein Beispiel. Geben Sie in der Implementierung des Controllers, in dem Sie das Drücken der Zurück-Taste ausführen möchten, an, dass das UINavigationBarDelegate-Protokoll implementiert wird:

@interface MyViewController () <UINavigationBarDelegate>

Machen Sie Ihren Controller dann irgendwo in Ihrem Initialisierungscode (wahrscheinlich in viewDidLoad) zum Delegaten seiner Navigationsleiste:

self.navigationController.navigationBar.delegate = self;

Implementieren Sie abschließend die Methode shouldPopItem. Diese Methode wird sofort aufgerufen, wenn die Zurück-Taste gedrückt wird. Wenn Sie mehrere Controller oder Navigationselemente im Stapel haben, möchten Sie wahrscheinlich überprüfen, welches dieser Navigationselemente angezeigt wird (der Elementparameter), damit Sie Ihre benutzerdefinierten Aufgaben nur dann ausführen, wenn Sie dies erwarten. Hier ist ein Beispiel:

-(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
hat bei mir nicht funktioniert .. schade, weil es schlank ist. "*** Beenden der App aufgrund der nicht erfassten Ausnahme 'NSInternalInconsistencyException', Grund: 'Der Delegat kann nicht manuell auf einer von einem Controller verwalteten UINavigationBar festgelegt werden.'"
DynamicDan

Dies funktioniert leider nicht mit einem UINavigationController. Stattdessen benötigen Sie einen Standard-UIViewController mit einer UINavigationBar. Dies bedeutet, dass Sie nicht mehrere der automatischen Viewcontroller nutzen können, die Ihnen der NavigationController bietet. Es tut uns leid!
Carlos Guzman

Ich habe gerade die UINavigationBar anstelle des NavigationBarController verwendet und dann funktioniert es einwandfrei. Ich weiß, dass es sich bei der Frage um den NavigationBarController handelt, aber diese Lösung ist schlank.
Appsunited

3

Wenn Sie "viewWillDisappear" oder eine ähnliche Methode nicht verwenden können, versuchen Sie, UINavigationController in eine Unterklasse zu unterteilen. Dies ist die Header-Klasse:

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

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Implementierungsklasse:

#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

Auf der anderen Seite müssen Sie diesen viewController mit Ihrem benutzerdefinierten NavigationController verknüpfen. Führen Sie in Ihrer viewDidLoad-Methode für Ihren regulären viewController Folgendes aus:

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

3

Hier ist eine andere Möglichkeit, die ich implementiert habe (habe sie nicht mit einem Abwicklungsabschnitt getestet, aber wahrscheinlich nicht differenziert, wie andere in Bezug auf andere Lösungen auf dieser Seite angegeben haben), damit der übergeordnete Ansichts-Controller Aktionen ausführt, bevor die untergeordnete VC, die er gepusht hat wird vom Ansichtsstapel entfernt (ich habe dies ein paar Ebenen tiefer als der ursprüngliche UINavigationController verwendet). Dies kann auch verwendet werden, um Aktionen auszuführen, bevor die childVC ebenfalls gepusht wird. Dies hat den zusätzlichen Vorteil, dass Sie mit der Zurück-Schaltfläche des iOS-Systems arbeiten können, anstatt ein benutzerdefiniertes UIBarButtonItem oder UIButton erstellen zu müssen.

  1. Lassen Sie Ihren übergeordneten VC das UINavigationControllerDelegateProtokoll übernehmen und sich für delegierte Nachrichten registrieren:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
  2. Implementieren Sie diese UINavigationControllerDelegateInstanzmethode 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. Wenn Sie in der obigen UINavigationControllerDelegateInstanzmethode eine bestimmte Rückruffunktion angeben

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

Das funktioniert bei mir 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

Wenn Sie ein Storyboard verwenden und aus einem Push-Segment kommen, können Sie es auch einfach überschreiben shouldPerformSegueWithIdentifier:sender:.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.