Setting action for back button in navigation controller

167,862

Solution 1

Try putting this into the view controller where you want to detect the press:

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

Solution 2

I've implemented UIViewController-BackButtonHandler extension. It does not need to subclass anything, just put it into your project and override navigationShouldPopOnBackButton method in UIViewController class:

-(BOOL) navigationShouldPopOnBackButton {
    if(needsShowConfirmation) {
        // Show confirmation alert
        // ...
        return NO; // Ignore 'Back' button this time
    }
    return YES; // Process 'Back' button click and Pop view controler
}

Download sample app.

Solution 3

Unlike Amagrammer said, it's possible. You have to subclass your navigationController. I explained everything here (including example code).

Solution 4

Swift Version:

(of https://stackoverflow.com/a/19132881/826435)

In your view controller you just conform to a protocol and perform whatever action you need:

extension MyViewController: NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool {
        performSomeActionOnThePressOfABackButton()
        return false
    }
}

Then create a class, say NavigationController+BackButton, and just copy-paste the code below:

protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress() -> Bool
}

extension UINavigationController {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        // Prevents from a synchronization issue of popping too many navigation items
        // and not enough view controllers or viceversa from unusual tapping
        if viewControllers.count < navigationBar.items!.count {
            return true
        }

        // Check if we have a view controller that wants to respond to being popped
        var shouldPop = true
        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
            shouldPop = viewController.shouldPopOnBackButtonPress()
        }

        if (shouldPop) {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            // Prevent the back button from staying in an disabled state
            for view in navigationBar.subviews {
                if view.alpha < 1.0 {
                    UIView.animate(withDuration: 0.25, animations: {
                        view.alpha = 1.0
                    })
                }
            }

        }

        return false
    }
}

Solution 5

It isn't possible to do directly. There are a couple alternatives:

  1. Create your own custom UIBarButtonItem that validates on tap and pops if the test passes
  2. Validate the form field contents using a UITextField delegate method, such as -textFieldShouldReturn:, which is called after the Return or Done button is pressed on the keyboard

The downside of the first option is that the left-pointing-arrow style of the back button cannot be accessed from a custom bar button. So you have to use an image or go with a regular style button.

The second option is nice because you get the text field back in the delegate method, so you can target your validation logic to the specific text field sent to the delegate call-back method.

Share:
167,862

Related videos on Youtube

Parrots
Author by

Parrots

Freelance designer and developer, focusing on web and iOS apps. Gamer, runner, pilot, and all around nerd.

Updated on June 04, 2020

Comments

  • Parrots
    Parrots about 4 years

    I'm trying to overwrite the default action of the back button in a navigation controller. I've provided a target an action on the custom button. The odd thing is when assigning it though the backbutton attribute it doesn't pay attention to them and it just pops the current view and goes back to the root:

    UIBarButtonItem *backButton = [[UIBarButtonItem alloc] 
                                      initWithTitle: @"Servers" 
                                      style:UIBarButtonItemStylePlain 
                                      target:self 
                                      action:@selector(home)];
    self.navigationItem.backBarButtonItem = backButton;
    

    As soon as I set it through the leftBarButtonItem on the navigationItem it calls my action, however then the button looks like a plain round one instead of the arrowed back one:

    self.navigationItem.leftBarButtonItem = backButton;
    

    How can I get it to call my custom action before going back to the root view? Is there a way to overwrite the default back action, or is there a method that is always called when leaving a view (viewDidUnload doesn't do that)?

    • PartySoft
      PartySoft almost 13 years
      action:@selector(home)]; needs a : after the selector action:@selector(home:)]; otherwise it won't work
    • mbm29414
      mbm29414 almost 12 years
      @PartySoft That's not true unless the method is declared with the colon. It's perfectly valid to have buttons call selectors that don't take any parameters.
    • Sam
      Sam over 11 years
    • JohnK
      JohnK about 11 years
      Why wouldn't Apple provide a button with style shaped like a back button? Seems pretty obvious.
    • Jiri Volejnik
      Jiri Volejnik about 6 years
    • Taras
      Taras almost 5 years
      I did it this way show deсision
    • Miguel Gallego
      Miguel Gallego about 4 years
      I post this stackoverflow.com/a/62275894/4084902 to overwrite the default action of the back button
  • John Ballinger
    John Ballinger almost 15 years
    I agree you could photoshop and I think I might do this if I really wanted it but now have decided to change the look and feel a tiny bit to get this to work the way I want.
  • Amagrammer
    Amagrammer almost 15 years
    Yes, except that the actions are not triggered when they are attached to the backBarButtonItem. I don't know if this is a bug or a feature; it's possible that even Apple doesn't know. As for the photoshopping exercise, again, I would be wary that Apple would reject the app for misusing a canonical symbol.
  • Parrots
    Parrots almost 15 years
    Yeah the problem is I want it to look like the normal back button, just need it to call my custom action first...
  • Kuba Suder
    Kuba Suder over 14 years
    Apple's documentation (developer.apple.com/iphone/library/documentation/UIKit/…) says that "This class is not intended for subclassing". Though I'm not sure what they mean by this - they could mean "you shouldn't normally need to do that", or they could mean "we will reject your app if you mess with our controller"...
  • Adam Eberbach
    Adam Eberbach about 14 years
    This is certainly the only way to do it. Wish I could award you more points Hans!
  • JosephH
    JosephH almost 14 years
    Can you actually prevent a view from exiting using this method? What would you make the popViewControllerAnimated method return if you wanted the view not to exit?
  • HansPinckaers
    HansPinckaers almost 14 years
    Yeah, you can. Just don't call the superclass method in your implementation, be aware! You shouldn't do that, the user expects to go back in the navigation. What you can do is ask for an confirmation. According to Apples documentation popViewController returns: "The view controller that was popped from the stack." So when nothing is popped your should return nil;
  • Amagrammer
    Amagrammer over 13 years
    Apparently I was mistaken in my original response. It just goes to show, sometimes "good" answers are wrong answers...
  • boliva
    boliva over 12 years
    this is a slick, clean, nice and very well thought workaround
  • JOM
    JOM over 12 years
    Whoo, hope this is future-proof! Works now beautifully, could remove custom subclass, which was doing nothing but trapping popViewControllerAnimated == wasn't very reusable
  • The iOSDev
    The iOSDev about 12 years
    awesome answer @William this is great i think you have done alote research thanks!
  • matm
    matm almost 12 years
    +1 great hack, but does not offer control over animation of pop
  • SAHM
    SAHM over 11 years
    Doesn't work for me if I send a message to the delegate through a button and the delegate pops the controller - this still fires.
  • Chase Roberts
    Chase Roberts over 11 years
    Another problem is that you can't differentiate if the user pressed the back button or if you programatically called [self.navigationController popViewControllerAnimated:YES]
  • deepwinter
    deepwinter over 11 years
    @HansPickaers I think your answer about preventing a view from exiting may be somewhat incorrect. If I display a 'confirm' message from the subclasses implementation of popViewControllerAnimated:, the NavigationBar still animates up one level in the tree regardless of what I return. This seems to be because clicking the back button calls shouldPopNavigationItem on the nav bar. I am returning nil from my subclasses method as recommendd.
  • Shog9
    Shog9 over 11 years
    Heads-up: this answer has been merged in from a duplicate.
  • Shog9
    Shog9 over 11 years
    Heads-up: this answer has been merged in from a duplicate.
  • Shog9
    Shog9 over 11 years
    Heads-up: this answer has been merged in from a duplicate.
  • Shog9
    Shog9 over 11 years
    Heads-up: this answer has been merged in from a duplicate.
  • Shog9
    Shog9 over 11 years
    Heads-up: this answer has been merged in from a duplicate.
  • Shog9
    Shog9 over 11 years
    Heads-up: this answer has been merged in from a duplicate.
  • Howard Pautz
    Howard Pautz over 11 years
    This works good for simple storyboard/Nib views - if there's no code-controlled navigation (as delirious, JPK, and Chase Roberts mention), is it reliable to do say update/save SQLite stuff that's in a managed context ?
  • JohnK
    JohnK about 11 years
    Agreed, @ChaseRoberts. The the technique can't differentiate between the back button and a Cancel button that just pops the current navigation item.
  • bogen
    bogen almost 11 years
    Scrap everything else. Best answer. All the other answers suck
  • Ramiro González Maciel
    Ramiro González Maciel over 10 years
    This is the cleanest solution I've seen, better and simpler than using your own custom UIButton. Thanks!
  • B.S.
    B.S. over 10 years
    Does Apple allow to change shouldPopItem? I think that app can be rejected
  • onegray
    onegray over 10 years
    The navigationBar:shouldPopItem: isn't a private method since it's a part of UINavigationBarDelegate protocol.
  • Sam
    Sam over 10 years
    but does UINavigationController already implement the delegate (to return YES)? or will it in the future? subclassing is probably a safer option
  • Sam
    Sam over 10 years
    @onegray out of interest - why do you dispatch_async a pop? wouldn't returning shouldPop from the shouldPopItem method be enough?
  • onegray
    onegray over 10 years
    Originally, the UINavigationController had its own handler of this method, but now it is overridden by the category and it may not work properly. That’s why it needs to use an explicit call of the popViewController instead. The dispatch_async is needed to break the calling stack.
  • Sam
    Sam over 10 years
    Ah yes, I nipped back on here as I'd just worked that out :) Interestingly UINavigationController doesn't respond to this method (it's a protocol that it doesn't seem to implement) - I did a quick check and there doesn't seem to be any method (even hidden). So if I use your method in a subclass, base calling just winds up in an infinite loop.
  • ambientlight
    ambientlight over 10 years
    @JosephH You can do it in your subclass, but not in this method. You have to make your subclass to be a delegate of UINavigationBarDelegate(UINavigationController doesn't implement it) Then you implement the method -navigationBar:shouldPopItem:, which will be called each time you press BackButton
  • C4 - Travis
    C4 - Travis over 10 years
    great answer, works for adding a transition and a pop animation.
  • arlomedia
    arlomedia over 10 years
    I think you mean to say subclass the UINavigationController class and implement a shouldPopItem method. That is working well for me. However, that method should not simply return YES or NO as you would expect. An explanation and solution is available here: stackoverflow.com/a/7453933/462162
  • boliva
    boliva over 10 years
    I just implemented this (pretty cool BTW) in iOS 7.1 and noticed that after returning NO the back button stays in a disabled state (visually, because it still receives and reacts to touch events). I worked around it by adding an else statement to the shouldPop check and cycling through the navigation bar subviews, and setting the alpha value back to 1 if needed inside an animation block: gist.github.com/idevsoftware/9754057
  • Srikanth
    Srikanth over 10 years
    This is one of the best extension which I have ever seen. Thank you so much.
  • frin
    frin over 10 years
    Excellent, I had issue with nested navigation controllers and had parent navigation bar hidden but wanted to show back arrow on child navigation bar, I accomplished this with dummy top view controller and pushed my view controller as second without animation. I overrode the back action so it pops on the parent navigation instead of showing the dummy view controller. Works great as intended.
  • Nick N
    Nick N about 10 years
    This would be a great one to become a cocoapod enabled project.
  • SmileBot
    SmileBot about 10 years
    This doesn't tell you the cause. An unwind segue could be the cause not a back button tap.
  • danfordham
    danfordham almost 10 years
    This is the best solution out there, that keeps the '< Back' button styling and doesn't require you to subclass UINavigationController.
  • Taskinul Haque
    Taskinul Haque almost 10 years
    this works well : as long as you don't need to call an AlertView on the back button push, it executes the command, but it continues with the back button press, while it does so
  • kjam
    kjam almost 10 years
    Wouldn't it be better to simply call original implementation if navigationShouldPopOnBackButton returns YES? My code already has a subclass of UINavigationController, so calling super is pretty straightforward. But for category case, you can do method swizzling in category's +load method, thus being able to save original implementation.
  • onegray
    onegray almost 10 years
    Yes, subclassing is an option and it is proposed in another answer. Using objc-runtime might be helpful, but it needs much more code, so not sure if it is reasonable.
  • Travis Elliott
    Travis Elliott almost 10 years
    This does work I suppose. However I need to pass data to the root view which is navigated to on the back button. How would I get that root view object so I can pass it data? This is easy in a prepareForSegue since you can get destinationViewController from the passed UIStoreboardSegue.
  • BHuelse
    BHuelse over 9 years
    This solution works great. No need to style overridden button
  • Nhat Dinh
    Nhat Dinh over 9 years
    This is a little bit treat. If for another reason , viewWillDisappear called -> the program will not work correctly. (Such as when i push a image picker controller -> viewWillDisappear will be called.)
  • anders
    anders over 9 years
    This is exactly what I was looking for! Thanks!
  • Mike Gledhill
    Mike Gledhill over 9 years
    Yup, a vital piece of functionality, brilliantly coded, and still missing in iOS 8. Great stuff.
  • hEADcRASH
    hEADcRASH over 9 years
    Just an FYI: Swift version: if (find(self.navigationController!.viewControllers as! [UIViewController],self)==nil)
  • James Parker
    James Parker about 9 years
    Why this is such a difficult thing to find and handle. Thank you! This is the answer for so many posts.
  • stkent
    stkent almost 9 years
    Neater swift: if !contains(navigationController.viewControllers as! [UIViewController], self)
  • iRiziya
    iRiziya almost 9 years
    Worked like a charm! Thanks :)
  • Justin Milo
    Justin Milo almost 9 years
    Make sure your view controller sets itself as the delegate of the inherited navigationController and conforms to the UINavigationControllerDelegate protocol
  • Pawel Cala
    Pawel Cala almost 9 years
    Overriding method navigationShouldPopOnBackButton in UIViewController doesn't work - Program executes parent method not the overriden one. Any solution for that? Does anyone have same issue?
  • testing
    testing almost 9 years
    DIdn't worked for me. In viewWillDisappear the current view controller is always removed from the navigation stack!
  • user2526811
    user2526811 over 8 years
    getting issue with iOS 7
  • Nick Weaver
    Nick Weaver over 8 years
    It's an old answer, but for those out there new to the topic, as of iOS 6 and later subclassing of UINaviationController is a viable option.
  • Pawriwes
    Pawriwes over 8 years
    its works like charm, but I could not convert into Swift version, can you please help me out.
  • Pawriwes
    Pawriwes over 8 years
    it goes all the way back to rootview if return true
  • kgaidis
    kgaidis over 8 years
    @Pawriwes Here's a solution I wrote up that seems to work for me: stackoverflow.com/a/34343418/826435
  • pronebird
    pronebird over 8 years
    This has issues with interactive pop gesture.
  • Frederik Winkelsdorf
    Frederik Winkelsdorf over 8 years
    I agree with Andy, this solution overwrites the UINavigationControllers own shouldPopItem method and breaks e.g. with NavigationControllers used in SplitViewController.
  • Manan Devani
    Manan Devani about 8 years
    This helped me in another way. I return No every time. Add my back arrow button and used popToRootViewController method.
  • Turvy
    Turvy over 7 years
    Maybe I missed something, but it does'nt work for me, the method performSomeActionOnThePressOfABackButton of the extension is never called
  • kgaidis
    kgaidis over 7 years
    @FlorentBreton maybe a misunderstanding? shouldPopOnBackButtonPress should get called as long as there are no bugs. performSomeActionOnThePressOfABackButton is just a made-up method that does not exist.
  • Turvy
    Turvy over 7 years
    I understood it, that's why I created a method performSomeActionOnThePressOfABackButton in my controller to execute a specific action when the back button is pressed, but this method was never called, the action is a normal back return
  • Tim Autin
    Tim Autin over 7 years
    Not working for me neither. The shouldPop method is never called. Did you set a delegate somewhere?
  • kgaidis
    kgaidis over 7 years
    @TimAutin I just tested this again and seems like something has changed. Key part to understand that in a UINavigationController, the navigationBar.delegate is set to the navigation controller. So the methods SHOULD get called. However, in Swift, I can't get them to be called, even in a subclass. I did, however, get them to be called in Objective-C, so I would just use the Objective-C version for now. Might be a Swift bug.
  • Murray Sagal
    Murray Sagal about 7 years
    As of iOS 10, and perhaps earlier, this is no longer working.
  • Murray Sagal
    Murray Sagal about 7 years
    Could you explain further how this proves the back button was tapped?
  • Chris Prince
    Chris Prince almost 7 years
    Working for me from Swift 3, iOS 10.3.
  • CMont
    CMont over 6 years
    This is simple, but it works only if you are sure to come back to that view from any child view you may load. If the child skips this view as it goes back to the parent, your code will not be called (the view had already disappeared without having moved from the parent). But that's the same issue with only handling events on trigger of the Back button as asked by the OP. So this is a simple answer to his question.
  • MarkAllen4512
    MarkAllen4512 over 6 years
    I am getting an error: Value of type 'UIViewController' has no member 'navigationShouldPopOnBackButton' when I try to compile your code, for the line if vc.responds(to: #selector(v... Also, the self.topViewController returns an optional and there is a warning for that also.
  • MarkAllen4512
    MarkAllen4512 over 6 years
    FWIW, I have fixed that code by making: let vc = self.topViewController as! MyViewController and it seem to work fine so far. If you believe that is a right change, you could edit the code. Also, if you feel that it should not be done, I will be glad to know why. Thanks for this code. You should probably write a blog post about this, as this answer is buried down as per the votes.
  • Lawliet
    Lawliet over 6 years
    @SankarP The reason you got that error is your MyViewController may not conform to BackButtonDelegate. Rather than forcing unwrap, you should do guard let vc = self.topViewController as? MyViewController else { return true } to avoid possible crash.
  • MarkAllen4512
    MarkAllen4512 over 6 years
    Thanks. I think the guard statement should become: guard let vc = self.topViewController as? MyViewController else { self.popViewController(animated: true) return true } to make sure that the screen is moving to the right page in case it cannot be rightly cast. I understand now that the navigationBar function is called in all VCs and not just the viewcontroller where this code is existing. May be it will be good to update the code in your answer too ? Thanks.
  • Kachi
    Kachi over 6 years
    This is one of my favorite StackOverflow answers of all-time!! Life-saver!
  • Kevin LeStarge
    Kevin LeStarge almost 5 years
    @FlorentBreton I also must have missed something. Nothing happens for me either.
  • H4Hugo
    H4Hugo over 4 years
    Can you provide details about what's going on with the if viewControllers.count < navigationBar.items!.count { return true } check please?
  • Siempay
    Siempay over 4 years
    // Prevents from a synchronization issue of popping too many navigation items // and not enough view controllers or viceversa from unusual tapping
  • Phontaine Judd
    Phontaine Judd over 4 years
    This is super simple and elegant. I love it. Just one problem: this will also fire if user swipes to go back, even if they cancel midway through. Perhaps a better solution would be to put this code into viewDidDisappear. That way it will only fire once view is definitely gone.