76

I need to detect when the user taps the "back" button on the navigation bar, in order to perform some operations when that occurs. I'm trying to set manually an action to such button, this way:

[self.navigationItem.backBarButtonItem setAction:@selector(performBackNavigation:)];

- (void)performBackNavigation:(id)sender
{
   // Do operations

   [self.navigationController popViewControllerAnimated:NO];
}

I firstly placed that code in the view controller itself, but I found that self.navigationItem.backBarButtonItem seemed to be nil, so I moved that same code to the parent view controller, which pushes the former to the navigation stack. But I'm neither able to make it work. I've read some posts regarding this issue, and some of them said that the selector needs to be set at the parent view controller, but for me it doesn't work anyway... What could I'm doing wrong?

Thanks

AppsDev
  • 12,319
  • 23
  • 93
  • 186

10 Answers10

131

Try this code using VIewWillDisappear method to detect the press of The back button of NavigationItem:

-(void) viewWillDisappear:(BOOL)animated
{
    if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) 
    {
        // Navigation button was pressed. Do some stuff 
        [self.navigationController popViewControllerAnimated:NO];
    }
    [super viewWillDisappear:animated];
}

OR There is another way to get Action of the Navigation BAck button.

Create Custom button for UINavigationItem of back button .

For Ex:

In ViewDidLoad :

- (void)viewDidLoad 
{
    [super viewDidLoad];
    UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle:@"Home" style:UIBarButtonItemStyleBordered target:self action:@selector(home:)];
    self.navigationItem.leftBarButtonItem=newBackButton;
}

-(void)home:(UIBarButtonItem *)sender 
{
    [self.navigationController popToRootViewControllerAnimated:YES];
}

Swift :

override func willMoveToParentViewController(parent: UIViewController?) 
{
    if parent == nil 
    {
        // Back btn Event handler
    }
}
P.J.Radadiya
  • 1,493
  • 1
  • 12
  • 21
Kumar KL
  • 15,315
  • 9
  • 38
  • 60
  • 3
    I don't think you need [self.navigationController popViewControllerAnimated:NO] in viewWillDisappear. – Robert Kang Aug 08 '14 at 12:09
  • 2
    I think this method does not work anymore with iOS 8 as self.navigationController.viewControllers contains only one element, == self – Sébastien Stormacq Oct 02 '14 at 20:21
  • 1
    @Sébastien Stormacq Why you say that? It work in iOS 8. – 蘇健豪 Jan 21 '15 at 12:26
  • 1
    If you call this in ViewWillDisappear, it won't be called just when back button is tapped. It will be called whenever the VC is being popped or whenever a new VC is being pushed – NSNoob Jul 18 '16 at 05:23
41

Swift

override func didMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        //"Back pressed"
    }
}
Ashish Kakkad
  • 23,586
  • 12
  • 103
  • 136
dadachi
  • 639
  • 8
  • 11
  • 5
    Only problem with this solution, is if you swipe to go back, and change your mind, this will get triggered. – chris P Mar 29 '16 at 21:23
18

Perhaps this answers doesn't fit your explanation but question title. It's useful when you are trying to know when you tapped the back button on an UINavigationBar.

In this case you can use UINavigationBarDelegate protocol and implement one of this methods:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item;
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item;

When didPopItem method is called, it's because you either tapped the back button or you used [UINavigationBar popNavigationItemAnimated:] method and the navigation bar did pop the item.

Now, if you want to know what action triggered the didPopItem method you can use a flag.

With this approach I don't need to manually add a left bar button item with an arrow image in order to make it similar to iOS back button, and be able to set my custom target/action.


Let's see an example:

I have a view controller that has a page view controller, and a custom page indicator view. I'm also using a custom UINavigationBar to display a title to know on what page am I and the back button to go back to the previous page. And I also can swipe to previous/next page on page controller.

#pragma mark - UIPageViewController Delegate Methods
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {

    if( completed ) {

        //...

        if( currentIndex > lastIndex ) {

            UINavigationItem *navigationItem = [[UINavigationItem alloc] initWithTitle:@"Some page title"];

            [[_someViewController navigationBar] pushNavigationItem:navigationItem animated:YES];
            [[_someViewController pageControl] setCurrentPage:currentIndex];
        } else {
            _autoPop = YES; //We pop the item automatically from code.
            [[_someViewController navigationBar] popNavigationItemAnimated:YES];
            [[_someViewController pageControl] setCurrentPage:currentIndex];
        }
    }

}

So then I implement UINavigationBar delegate methods:

#pragma mark - UINavigationBar Delegate Methods
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
    if( !_autoPop ) {
        //Pop by back button tap
    } else {
        //Pop from code
    }

    _autoPop = NO;

    return YES;
}

In this case I used shouldPopItem because the pop is animated and I wanted to handle the back button immediately and not to wait until transition is finished.

Firula
  • 1,251
  • 10
  • 29
12

The problem with didMoveToParentViewController it's that it gets called once the parent view is fully visible again so if you need to perform some tasks before that, it won't work.

And it doesn't work with the driven animation gesture. Using willMoveToParentViewController works better.

Objective-c

- (void)willMoveToParentViewController:(UIViewController *)parent{
    if (parent == NULL) {
        // ...
    }
}

Swift

override func willMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        // ...  
    }
}
Nico
  • 6,269
  • 9
  • 45
  • 85
6

This is Objective-C version of dadachi's Answer :

Objective-C

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == NULL) {
        NSLog(@"Back Pressed");
    }
}
Community
  • 1
  • 1
Ashish Kakkad
  • 23,586
  • 12
  • 103
  • 136
3

Set the UINavigationBar's delegate, and then use:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
    //handle the action here
}
Jeffrey Sun
  • 7,789
  • 1
  • 24
  • 17
  • 2
    If you are using a `UINavigationController` to manage the navigation bar then trying to set the delegate causes an exception: "*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Cannot manually set the delegate on a UINavigationBar managed by a controller.'". The `UINavigationController` is the delegate. Which means you can sub-class the controller and override the `UINavigationBarDelegate` methods (probably calling super). – Geoff Hackworth Jul 26 '14 at 10:36
  • But you can't directly call super because `UINavigationController` doesn't publicly conform to `UINavigationBarDelegate`, resulting in a compiler error! There might be a solution using `UINavigationControllerDelegate`. – Geoff Hackworth Jul 26 '14 at 10:47
3

None of the other solutions worked for me, but this does:

Create your own subclass of UINavigationController, make it implement the UINavigationBarDelegate (no need to manually set the navigation bar's delegate), add a UIViewController extension that defines a method to be called on a back button press, and then implement this method in your UINavigationController subclass:

func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
    self.topViewController?.methodToBeCalledOnBackButtonPress()
    self.popViewController(animated: true)
    return true
}
Shady
  • 31
  • 1
3

In Swift 4 or above:

override func didMove(toParent parent: UIViewController?) {
    if parent == nil {
        //"Back pressed"
    }
}
iMichele
  • 51
  • 7
2

Set the UINavigationControllerDelegate and implement this delegate func (Swift):

func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
    if viewController is <target class> {
        //if the only way to get back - back button was pressed
    }
}
mark
  • 181
  • 6
1

Use a custom UINavigationController subclass, which implements the shouldPop method.

In Swift:

class NavigationController: UINavigationController, UINavigationBarDelegate
{
    var shouldPopHandler: (() -> Bool)?

    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool
    {
        if let shouldPopHandler = self.shouldPopHandler, !shouldPopHandler()
        {
            return false
        }
        self.popViewController(animated: true) // Needed!
        return true
    }
}

When set, your shouldPopHandler() will be called to decide whether the controller will be pop or not. When not set it will just get popped as usual.

It is a good idea to disable UINavigationControllers interactivePopGestureRecognizer as the gesture won't call your handler otherwise.

Rivera
  • 10,792
  • 3
  • 58
  • 102