103

I have pushed a view onto the navigation controller and when I press the back button it goes to the previous view automatically. I want to do a few things when back button is pressed before popping the view off the stack. Which is the back button callback function?

Namratha
  • 15,910
  • 25
  • 86
  • 123
  • possible duplicate of [Setting action for back button in navigation controller](http://stackoverflow.com/questions/1214965/setting-action-for-back-button-in-navigation-controller) – Zakaria Mar 05 '13 at 06:52
  • Checkout this [solution][1] which retains the back button style as well. [1]: http://stackoverflow.com/a/29943156/3839641 – Sarasranglt Apr 29 '15 at 12:06

12 Answers12

163

William Jockusch's answer solve this problem with easy 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];
}
Community
  • 1
  • 1
ymutlu
  • 5,973
  • 4
  • 30
  • 47
  • 32
    This code is not only executed when user taps back button, but in every event the view is popped (e.g. when having a done or save button at the right-side). – meaning-matters Apr 25 '13 at 19:34
  • 7
    Or when moving forward to a new view. – GuybrushThreepwood Dec 02 '13 at 10:00
  • This is also called when the user pans from the left edge (interactivePopGestureRecognizer). In my case, I'm specifically looking for when the user presses back while NOT panning from the left edge. – Kyle Clegg Mar 20 '14 at 00:11
  • 2
    Doesn't mean the back button was the cause. Could be an unwind segue for instance. – smileBot Jun 27 '14 at 18:26
  • Is there a reason why you call the **super** function after the **if**? As far as I know, it is supposed to be called as the very first line. Look the answer at this post: http://stackoverflow.com/a/3348425/1035008 – Yuchen Mar 30 '15 at 14:59
  • 1
    I have a doubt, Why shouldn't we do this in viewDidDisappear? – JohnVanDijk Aug 05 '15 at 07:40
85

In my opinion the best solution.

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

But it only works with iOS5+

Blank
  • 4,658
  • 2
  • 22
  • 24
  • 3
    This technique can't distinguish between a back button tap and an unwind segue. – smileBot Jun 27 '14 at 18:25
  • The willMoveToParentViewController and viewWillDisappear method does not explain the controller must be destroyed, didMoveToParentViewController is right – Hank Sep 22 '15 at 05:36
27

it's probably better to override the backbutton so you can handle the event before the view is popped for things such as user confirmation.

in viewDidLoad create a UIBarButtonItem and set self.navigationItem.leftBarButtonItem to it passing in a 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];

}

Then you can do things like raise an UIAlertView to confirm the action, then pop the view controller, etc.

Or instead of creating a new backbutton, you can conform to the UINavigationController delegate methods to do actions when the back button is pressed.

roocell
  • 2,369
  • 24
  • 28
11

Maybe it's a little too late, but I also wanted the same behavior before. And the solution I went with works quite well in one of the apps currently on the App Store. Since I haven't seen anyone goes with similar method, I would like to share it here. The downside of this solution is that it requires subclassing UINavigationController. Though using Method Swizzling might help avoiding that, I didn't go that far.

So, the default back button is actually managed by UINavigationBar. When a user taps on the back button, UINavigationBar ask its delegate if it should pop the top UINavigationItem by calling navigationBar(_:shouldPop:). UINavigationController actually implement this, but it doesn't publicly declare that it adopts UINavigationBarDelegate (why!?). To intercept this event, create a subclass of UINavigationController, declare its conformance to UINavigationBarDelegate and implement navigationBar(_:shouldPop:). Return true if the top item should be popped. Return false if it should stay.

There are two problems. The first is that you must call the UINavigationController version of navigationBar(_:shouldPop:) at some point. But UINavigationBarController doesn't publicly declare it conformance to UINavigationBarDelegate, trying to call it will result in a compile time error. The solution I went with is to use Objective-C runtime to get the implementation directly and call it. Please let me know if anyone has a better solution.

The other problem is that navigationBar(_:shouldPop:) is called first follows by popViewController(animated:) if the user taps on the back button. The order reverses if the view controller is popped by calling popViewController(animated:). In this case, I use a boolean to detect if popViewController(animated:) is called before navigationBar(_:shouldPop:) which mean that the user has tapped on the back button.

Also, I make an extension of UIViewController to let the navigation controller ask the view controller if it should be popped if the user taps on the back button. View controllers can return false and do any necessary actions and call popViewController(animated:) later.

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

And in you view controllers, implement shouldBePopped(_:). If you don't implement this method, the default behavior will be to pop the view controller as soon as the user taps on the back button just like normal.

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

You can look at my demo here.

enter image description here

yusuke024
  • 1,994
  • 16
  • 12
  • This is an awesome solution and should be baked into a blogpost! Seems to be overkill for what I am searching right now, but in other circumstances, this is sure well worth trying. – ASSeeger Jan 24 '20 at 10:02
9

I end up with this solutions. As we tap back button viewDidDisappear method called. we can check by calling isMovingFromParentViewController selector which return true. we can pass data back (Using Delegate).hope this help someone.

-(void)viewDidDisappear:(BOOL)animated{

    if (self.isMovingToParentViewController) {

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

    }
}
Avijit Nagare
  • 7,801
  • 7
  • 34
  • 62
9

This is the correct way to detect this.

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

    }
}

this method is called when view is pushed as well. So checking parent==nil is for popping view controller from stack

Saad
  • 8,501
  • 2
  • 36
  • 49
6

For "BEFORE popping the view off the stack" :

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil){
        NSLog(@"do whatever you want here");
    }
}
Anum Malik
  • 484
  • 6
  • 11
5

There's a more appropriate way than asking the viewControllers. You can make your controller a delegate of the navigationBar that has the back button. Here's an example. In the implementation of the controller where you want to handle the press of the back button, tell it that it will implement the UINavigationBarDelegate protocol:

@interface MyViewController () <UINavigationBarDelegate>

Then somewhere in your initialization code (probably in viewDidLoad) make your controller the delegate of its navigation bar:

self.navigationController.navigationBar.delegate = self;

Finally, implement the shouldPopItem method. This method gets called right when the back button is pressed. If you have multiple controllers or navigation Items in the stack, you'll probably want to check which of those navigation items is getting popped (the item parameter), so that you only do your custom stuff when you expect to. Here's an example:

-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    NSLog(@"Back button got pressed!");
    //if you return NO, the back button press is cancelled
    return YES;
}
Carlos Guzman
  • 409
  • 6
  • 12
  • 4
    didn't work for me.. pitty because it's lean. "*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot manually set the delegate on a UINavigationBar managed by a controller.'" – DynamicDan Dec 24 '13 at 17:23
  • This will unfortunately not work with a UINavigationController, instead, you need a standard UIViewController with a UINavigationBar in it. This does mean you can't take advantage of several of the automatic viewcontroller pushing and popping that the NavigationController gives you. Sorry! – Carlos Guzman Jan 06 '14 at 15:51
  • I just used the UINavigationBar instead of the NavigationBarController and then it works fine. I know the question is about the NavigationBarController, but this solution is lean. – appsunited Nov 24 '14 at 15:49
3

If you can't use "viewWillDisappear" or similar method, try to subclass UINavigationController. This is the header class:

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

@interface CCNavigationController : UINavigationController

@property (nonatomic, strong) MyViewController *viewController;

@end

Implementation class:

#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

In the other hand, you need to link this viewController to your custom NavigationController, so, in your viewDidLoad method for your regular viewController do this:

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

Here's another way I implemented (didn't test it with an unwind segue but it probably wouldn't differentiate, as others have stated in regards to other solutions on this page) to have the parent view controller perform actions before the child VC it pushed gets popped off the view stack (I used this a couple levels down from the original UINavigationController). This could also be used to perform actions before the childVC gets pushed, too. This has the added advantage of working with the iOS system back button, instead of having to create a custom UIBarButtonItem or UIButton.

  1. Have your parent VC adopt the UINavigationControllerDelegate protocol and register for delegate messages:

    MyParentViewController : UIViewController <UINavigationControllerDelegate>
    
    -(void)viewDidLoad {
        self.navigationcontroller.delegate = self;
    }
    
  2. Implement this UINavigationControllerDelegate instance method 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. If you specify a specific callback function in the above UINavigationControllerDelegate instance method

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

    }

Evan R
  • 793
  • 11
  • 25
1

This is what it works for 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)
}
pableiros
  • 10,752
  • 11
  • 79
  • 78
0

If you're using a Storyboard and you're coming from a push segue, you could also just override shouldPerformSegueWithIdentifier:sender:.

Mojo66
  • 969
  • 9
  • 18