181

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)?

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Parrots
  • 26,658
  • 14
  • 59
  • 78
  • action:@selector(home)]; needs a : after the selector action:@selector(home:)]; otherwise it won't work – PartySoft Sep 18 '11 at 09:00
  • 7
    @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. – mbm29414 Sep 07 '12 at 13:50
  • possible duplicate of [Stopping the self.navigationItem.leftBarButtonItem from exiting a view](http://stackoverflow.com/questions/1184474/stopping-the-self-navigationitem-leftbarbuttonitem-from-exiting-a-view) – user7116 Feb 21 '13 at 01:52
  • 3
    Why wouldn't Apple provide a button with style shaped like a back button? Seems pretty obvious. – JohnK Jun 28 '13 at 16:00
  • Look at the [solution in this thread](https://stackoverflow.com/questions/50750456/exception-cannot-manually-set-the-delegate-on-a-uinavigationbar-managed-by-a-co/50925934#50925934) – Jiri Volejnik Jun 19 '18 at 11:03
  • I did it this way [show deсision](https://stackoverflow.com/a/57137294/11079607) – Taras Jul 21 '19 at 22:05
  • I post this https://stackoverflow.com/a/62275894/4084902 to overwrite the default action of the back button – Miguel Gallego Jun 09 '20 at 06:05

29 Answers29

363

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];
}
William Jockusch
  • 26,513
  • 49
  • 182
  • 323
  • 1
    this is a slick, clean, nice and very well thought workaround – boliva Nov 25 '11 at 16:19
  • Whoo, hope this is future-proof! Works now beautifully, could remove custom subclass, which was doing nothing but trapping popViewControllerAnimated == wasn't very reusable – JOM Mar 01 '12 at 06:15
  • awesome answer @William this is great i think you have done alote research thanks! – The iOSDev Apr 09 '12 at 10:05
  • 7
    +1 great hack, but does not offer control over animation of pop – matm Jul 06 '12 at 10:27
  • 3
    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. – SAHM Oct 12 '12 at 19:45
  • 21
    Another problem is that you can't differentiate if the user pressed the back button or if you programatically called [self.navigationController popViewControllerAnimated:YES] – Chase Roberts Dec 27 '12 at 18:00
  • Heads-up: this answer has been merged in from a duplicate. – Shog9 Feb 23 '13 at 18:09
  • 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 ? – Howard Pautz Mar 16 '13 at 02:31
  • Agreed, @ChaseRoberts. The the technique can't differentiate between the back button and a Cancel button that just pops the current navigation item. – JohnK Jun 28 '13 at 15:44
  • Scrap everything else. Best answer. All the other answers suck – bogen Aug 07 '13 at 13:22
  • great answer, works for adding a transition and a pop animation. – C4 - Travis Jan 28 '14 at 00:10
  • This doesn't tell you the cause. An unwind segue could be the cause not a back button tap. – SmileBot Jun 27 '14 at 18:30
  • 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 – Taskinul Haque Aug 01 '14 at 19:08
  • 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. – Travis Elliott Aug 25 '14 at 15:29
  • 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.) – Nhat Dinh Dec 04 '14 at 07:31
  • 10
    Just an FYI: Swift version: `if (find(self.navigationController!.viewControllers as! [UIViewController],self)==nil)` – hEADcRASH Mar 25 '15 at 18:43
  • 1
    Neater swift: `if !contains(navigationController.viewControllers as! [UIViewController], self)` – stkent Jul 27 '15 at 13:11
  • DIdn't worked for me. In `viewWillDisappear` the current view controller is always removed from the navigation stack! – testing Sep 18 '15 at 09:01
  • getting issue with iOS 7 – user2526811 Oct 01 '15 at 07:54
181

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

Download sample app.

onegray
  • 5,105
  • 1
  • 23
  • 29
  • Does Apple allow to change shouldPopItem? I think that app can be rejected – B.S. Oct 10 '13 at 12:29
  • 4
    The `navigationBar:shouldPopItem:` isn't a private method since it's a part of `UINavigationBarDelegate` protocol. – onegray Oct 10 '13 at 13:52
  • 1
    but does UINavigationController already implement the delegate (to return `YES`)? or will it in the future? subclassing is probably a safer option – Sam Nov 15 '13 at 11:23
  • 1
    @onegray out of interest - why do you `dispatch_async` a pop? wouldn't returning `shouldPop` from the `shouldPopItem` method be enough? – Sam Nov 15 '13 at 12:13
  • 1
    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. – onegray Nov 15 '13 at 13:49
  • 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. – Sam Nov 15 '13 at 17:54
  • 8
    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: https://gist.github.com/idevsoftware/9754057 – boliva Mar 25 '14 at 02:12
  • 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. – frin Mar 26 '14 at 09:13
  • This would be a great one to become a cocoapod enabled project. – Nick N May 05 '14 at 16:09
  • This is the best solution out there, that keeps the '< Back' button styling and doesn't require you to subclass UINavigationController. – danfordham Jul 29 '14 at 16:38
  • 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. – kjam Aug 07 '14 at 14:31
  • 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. – onegray Aug 07 '14 at 19:41
  • This has issues with interactive pop gesture. – pronebird Jan 31 '16 at 11:26
  • I agree with Andy, this solution overwrites the UINavigationControllers own shouldPopItem method and breaks e.g. with NavigationControllers used in SplitViewController. – Frederik Winkelsdorf Mar 06 '16 at 11:54
  • This helped me in another way. I return No every time. Add my back arrow button and used popToRootViewController method. – Manan Devani Jun 04 '16 at 05:19
40

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

Rishil Patel
  • 1,977
  • 3
  • 14
  • 30
HansPinckaers
  • 1,755
  • 15
  • 18
  • Apple's documentation (http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UINavigationController_Class/Reference/Reference.html) 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"... – Kuba Suder Feb 11 '10 at 15:41
  • This is certainly the only way to do it. Wish I could award you more points Hans! – Adam Eberbach Apr 12 '10 at 01:53
  • 1
    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? – JosephH Aug 26 '10 at 17:30
  • 1
    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; – HansPinckaers Aug 26 '10 at 19:04
  • Apparently I was mistaken in my original response. It just goes to show, sometimes "good" answers are wrong answers... – Amagrammer Oct 12 '10 at 15:35
  • 1
    @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. – deepwinter Jan 26 '13 at 00:20
  • Heads-up: this answer has been merged in from a duplicate. – Shog9 Feb 23 '13 at 18:09
  • @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 – ambientlight Dec 06 '13 at 12:44
  • 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. – Nick Weaver Oct 27 '15 at 11:59
17

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
    }
}
Community
  • 1
  • 1
kgaidis
  • 14,259
  • 4
  • 79
  • 93
  • 1
    Maybe I missed something, but it does'nt work for me, the method performSomeActionOnThePressOfABackButton of the extension is never called – Turvy Nov 30 '16 at 14:12
  • @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. – kgaidis Nov 30 '16 at 14:44
  • 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 – Turvy Nov 30 '16 at 16:10
  • 2
    Not working for me neither. The shouldPop method is never called. Did you set a delegate somewhere? – Tim Autin Jan 15 '17 at 19:12
  • @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. – kgaidis Jan 15 '17 at 22:37
  • @FlorentBreton I also must have missed something. Nothing happens for me either. – Kevin LeStarge Jul 16 '19 at 16:05
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.

Alex Reynolds
  • 95,983
  • 54
  • 240
  • 345
5

For some threading reasons, the solution mentionned by @HansPinckaers wasn't right for me, but I found a very easier way to catch a touch on the back button, and I wanna pin this down here in case this could avoid hours of deceptions for someone else. The trick is really easy : just add a transparent UIButton as a subview to your UINavigationBar, and set your selectors for him as if it was the real button! Here's an example using Monotouch and C#, but the translation to objective-c shouldn't be too hard to find.

public class Test : UIViewController {
    public override void ViewDidLoad() {
        UIButton b = new UIButton(new RectangleF(0, 0, 60, 44)); //width must be adapted to label contained in button
        b.BackgroundColor = UIColor.Clear; //making the background invisible
        b.Title = string.Empty; // and no need to write anything
        b.TouchDown += delegate {
            Console.WriteLine("caught!");
            if (true) // check what you want here
                NavigationController.PopViewControllerAnimated(true); // and then we pop if we want
        };
        NavigationController.NavigationBar.AddSubview(button); // insert the button to the nav bar
    }
}

Fun fact : for testing purposes and to find good dimensions for my fake button, I set its background color to blue... And it shows behind the back button! Anyway, it still catches any touch targetting the original button.

psycho
  • 631
  • 10
  • 27
4

Overriding navigationBar(_ navigationBar:shouldPop): This is not a good idea, even if it works. for me it generated random crashes on navigating back. I advise you to just override the back button by removing the default backButton from navigationItem and creating a custom back button like below:

override func viewDidLoad(){
   super.viewDidLoad()
   
   navigationItem.leftBarButton = .init(title: "Go Back", ... , action: #selector(myCutsomBackAction) 

   ...
 
}

========================================

Building on previous responses with UIAlert in Swift5 in a Asynchronous way


protocol NavigationControllerBackButtonDelegate {
    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ())
}

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
      
        if viewControllers.count < navigationBar.items!.count {
            return true
        }
        
        // Check if we have a view controller that wants to respond to being popped
        
        if let viewController = topViewController as? NavigationControllerBackButtonDelegate {
            
            viewController.shouldPopOnBackButtonPress { shouldPop in
                if (shouldPop) {
                    /// on confirm => pop
                    DispatchQueue.main.async {
                        self.popViewController(animated: true)
                    }
                } else {
                    /// on cancel => do nothing
                }
            }
            /// return false => so navigator will cancel the popBack
            /// until user confirm or cancel
            return false
        }else{
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        }
        return true
    }
}


On your controller


extension MyController: NavigationControllerBackButtonDelegate {
    
    func shouldPopOnBackButtonPress(_ completion: @escaping (Bool) -> ()) {
    
        let msg = "message"
        
        /// show UIAlert
        alertAttention(msg: msg, actions: [
            
            .init(title: "Continuer", style: .destructive, handler: { _ in
                completion(true)
            }),
            .init(title: "Annuler", style: .cancel, handler: { _ in
                completion(false)
            })
            ])
   
    }

}
Community
  • 1
  • 1
Siempay
  • 876
  • 1
  • 11
  • 32
  • Can you provide details about what's going on with the if viewControllers.count < navigationBar.items!.count { return true } check please? – H4Hugo Oct 09 '19 at 08:06
  • // Prevents from a synchronization issue of popping too many navigation items // and not enough view controllers or viceversa from unusual tapping – Siempay Oct 18 '19 at 20:59
3

Easiest way

You can use the UINavigationController's delegate methods. The method willShowViewController is called when the back button of your VC is pressed.do whatever you want when back btn pressed

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
Zar E Ahmer
  • 33,936
  • 20
  • 234
  • 300
  • Make sure your view controller sets itself as the delegate of the inherited navigationController and conforms to the UINavigationControllerDelegate protocol – Justin Milo Aug 26 '15 at 08:05
3

Here's my Swift solution. In your subclass of UIViewController, override the navigationShouldPopOnBackButton method.

extension UIViewController {
    func navigationShouldPopOnBackButton() -> Bool {
        return true
    }
}

extension UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {
        if let vc = self.topViewController {
            if vc.navigationShouldPopOnBackButton() {
                self.popViewControllerAnimated(true)
            } else {
                for it in navigationBar.subviews {
                    let view = it as! UIView
                    if view.alpha < 1.0 {
                        [UIView .animateWithDuration(0.25, animations: { () -> Void in
                            view.alpha = 1.0
                        })]
                    }
                }
                return false
            }
        }
        return true
    }

}
Michael
  • 32,527
  • 49
  • 210
  • 370
AutomatonTec
  • 666
  • 6
  • 15
  • 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? – Pawel Cala Aug 27 '15 at 15:17
  • it goes all the way back to rootview if return true – Pawriwes Dec 04 '15 at 18:06
  • @Pawriwes Here's a solution I wrote up that seems to work for me: http://stackoverflow.com/a/34343418/826435 – kgaidis Dec 17 '15 at 20:16
3

Found a solution which retains the back button style as well. Add the following method to your view controller.

-(void) overrideBack{

    UIButton *transparentButton = [[UIButton alloc] init];
    [transparentButton setFrame:CGRectMake(0,0, 50, 40)];
    [transparentButton setBackgroundColor:[UIColor clearColor]];
    [transparentButton addTarget:self action:@selector(backAction:) forControlEvents:UIControlEventTouchUpInside];
    [self.navigationController.navigationBar addSubview:transparentButton];


}

Now provide a functionality as needed in the following method:

-(void)backAction:(UIBarButtonItem *)sender {
    //Your functionality
}

All it does is to cover the back button with a transparent button ;)

Sarasranglt
  • 3,819
  • 4
  • 23
  • 36
3

This technique allows you to change the text of the "back" button without affecting the title of any of the view controllers or seeing the back button text change during the animation.

Add this to the init method in the calling view controller:

UIBarButtonItem *temporaryBarButtonItem = [[UIBarButtonItem alloc] init];   
temporaryBarButtonItem.title = @"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
Jason Moore
  • 7,169
  • 1
  • 44
  • 45
2

I don't believe this is possible, easily. The only way I believe to get around this is to make your own back button arrow image to place up there. It was frustrating for me at first but I see why, for consistency's sake, it was left out.

You can get close (without the arrow) by creating a regular button and hiding the default back button:

self.navigationItem.leftBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:@"Servers" style:UIBarButtonItemStyleDone target:nil action:nil] autorelease];
self.navigationItem.hidesBackButton = YES;
Meltemi
  • 37,979
  • 50
  • 195
  • 293
  • 2
    Yeah the problem is I want it to look like the normal back button, just need it to call my custom action first... – Parrots Aug 01 '09 at 01:40
2

There's an easier way by just subclassing the delegate method of the UINavigationBar and override the ShouldPopItemmethod.

Max
  • 12,622
  • 16
  • 73
  • 101
  • 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: http://stackoverflow.com/a/7453933/462162 – arlomedia Mar 14 '14 at 15:35
2

This approach worked for me (but the "Back" button will not have the "<" sign):

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIBarButtonItem* backNavButton = [[UIBarButtonItem alloc] initWithTitle:@"Back"
                                                                      style:UIBarButtonItemStyleBordered
                                                                     target:self
                                                                     action:@selector(backButtonClicked)];
    self.navigationItem.leftBarButtonItem = backNavButton;
}

-(void)backButtonClicked
{
    // Do something...
    AppDelegate* delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
    [delegate.navController popViewControllerAnimated:YES];
}
Ivan
  • 472
  • 4
  • 15
2

onegray's solution is not safe.According to the official documents by Apple,https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html, we should avoid doing that.

"If the name of a method declared in a category is the same as a method in the original class, or a method in another category on the same class (or even a superclass), the behavior is undefined as to which method implementation is used at runtime. This is less likely to be an issue if you’re using categories with your own classes, but can cause problems when using categories to add methods to standard Cocoa or Cocoa Touch classes."

2

Using Swift:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}
Murray Sagal
  • 8,454
  • 4
  • 47
  • 48
2

Here is Swift 3 version of @oneway's answer for catching navigation bar back button event before it gets fired. As UINavigationBarDelegate cannot be used for UIViewController, you need to create a delegate that will be triggered when navigationBar shouldPop is called.

@objc public protocol BackButtonDelegate {
      @objc optional func navigationShouldPopOnBackButton() -> Bool 
}

extension UINavigationController: UINavigationBarDelegate  {

    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

        if viewControllers.count < (navigationBar.items?.count)! {                
            return true
        }

        var shouldPop = true
        let vc = self.topViewController

        if vc.responds(to: #selector(vc.navigationShouldPopOnBackButton)) {
            shouldPop = vc.navigationShouldPopOnBackButton()
        }

        if shouldPop {
            DispatchQueue.main.async {
                self.popViewController(animated: true)
            }
        } else {
            for subView in navigationBar.subviews {
                if(0 < subView.alpha && subView.alpha < 1) {
                    UIView.animate(withDuration: 0.25, animations: {
                        subView.alpha = 1
                    })
                }
            }
        }

        return false
    }
}

And then, in your view controller add the delegate function:

class BaseVC: UIViewController, BackButtonDelegate {
    func navigationShouldPopOnBackButton() -> Bool {
        if ... {
            return true
        } else {
            return false
        }        
    }
}

I've realised that we often want to add an alert controller for users to decide whether they wanna go back. If so, you can always return false in navigationShouldPopOnBackButton() function and close your view controller by doing something like this:

func navigationShouldPopOnBackButton() -> Bool {
     let alert = UIAlertController(title: "Warning",
                                          message: "Do you want to quit?",
                                          preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Yes", style: .default, handler: { UIAlertAction in self.yes()}))
            alert.addAction(UIAlertAction(title: "No", style: .cancel, handler: { UIAlertAction in self.no()}))
            present(alert, animated: true, completion: nil)
      return false
}

func yes() {
     print("yes")
     DispatchQueue.main.async {
            _ = self.navigationController?.popViewController(animated: true)
        }
}

func no() {
    print("no")       
}
Lawliet
  • 3,438
  • 2
  • 17
  • 28
  • 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. – Sankar Jan 18 '18 at 09:53
  • 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. – Sankar Jan 18 '18 at 10:04
  • @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. – Lawliet Jan 18 '18 at 21:51
  • 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. – Sankar Jan 19 '18 at 08:47
2

Swift 4 iOS 11.3 Version:

This builds on the answer from kgaidis from https://stackoverflow.com/a/34343418/4316579

I am not sure when the extension stopped working, but at the time of this writing (Swift 4), it appears that the extension will no longer be executed unless you declare UINavigationBarDelegate conformity as described below.

Hope this helps people that are wondering why their extension no longer works.

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {

    }
}
Community
  • 1
  • 1
Edward L.
  • 564
  • 2
  • 9
1

By using the target and action variables that you are currently leaving 'nil', you should be able to wire your save-dialogs in so that they are called when the button is "selected". Watch out, this may get triggered at strange moments.

I agree mostly with Amagrammer, but I don't think it would be that hard to make the button with the arrow custom. I would just rename the back button, take a screen shot, photoshop the button size needed, and have that be the image on the top of your button.

TahoeWolverine
  • 1,749
  • 2
  • 23
  • 31
  • 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. – John Ballinger Jul 27 '09 at 01:38
  • 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. – Amagrammer Jul 27 '09 at 02:43
  • Heads-up: this answer has been merged in from a duplicate. – Shog9 Feb 23 '13 at 18:10
1

For a form that requires user input like this, I would recommend invoking it as a "modal" instead of part of your navigation stack. That way they have to take care of business on the form, then you can validate it and dismiss it using a custom button. You can even design a nav bar that looks the same as the rest of your app but gives you more control.

Travis M.
  • 10,930
  • 1
  • 56
  • 72
1

To intercept the Back button, simply cover it with a transparent UIControl and intercept the touches.

@interface MyViewController : UIViewController
{
    UIControl   *backCover;
    BOOL        inhibitBackButtonBOOL;
}
@end

@implementation MyViewController
-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // Cover the back button (cannot do this in viewWillAppear -- too soon)
    if ( backCover == nil ) {
        backCover = [[UIControl alloc] initWithFrame:CGRectMake( 0, 0, 80, 44)];
#if TARGET_IPHONE_SIMULATOR
        // show the cover for testing
        backCover.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.15];
#endif
        [backCover addTarget:self action:@selector(backCoverAction) forControlEvents:UIControlEventTouchDown];
        UINavigationBar *navBar = self.navigationController.navigationBar;
        [navBar addSubview:backCover];
    }
}

-(void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    [backCover removeFromSuperview];
    backCover = nil;
}

- (void)backCoverAction
{
    if ( inhibitBackButtonBOOL ) {
        NSLog(@"Back button aborted");
        // notify the user why...
    } else {
        [self.navigationController popViewControllerAnimated:YES]; // "Back"
    }
}
@end
Jeff
  • 2,659
  • 1
  • 22
  • 41
1

At least in Xcode 5, there is a simple and pretty good (not perfect) solution. In IB, drag a Bar Button Item off the Utilities pane and drop it on the left side of the Navigation Bar where the Back button would be. Set the label to "Back." You will have a functioning button that you can tie to your IBAction and close your viewController. I'm doing some work and then triggering an unwind segue and it works perfectly.

What isn't ideal is that this button does not get the < arrow and does not carry forward the previous VCs title, but I think this can be managed. For my purposes, I set the new Back button to be a "Done" button so it's purpose is clear.

You also end up with two Back buttons in the IB navigator, but it is easy enough to label it for clarity.

enter image description here

Dan Loughney
  • 4,647
  • 3
  • 25
  • 40
1

Swift

override func viewWillDisappear(animated: Bool) {
    let viewControllers = self.navigationController?.viewControllers!
    if indexOfArray(viewControllers!, searchObject: self) == nil {
        // do something
    }
    super.viewWillDisappear(animated)
}

func indexOfArray(array:[AnyObject], searchObject: AnyObject)-> Int? {
    for (index, value) in enumerate(array) {
        if value as UIViewController == searchObject as UIViewController {
            return index
        }
    }
    return nil
}
zono
  • 8,366
  • 21
  • 75
  • 113
1

You can try accessing the NavigationBars Right Button item and set its selector property...heres a reference UIBarButtonItem reference, another thing if this doenst work that will def work is, set the right button item of the nav bar to a custom UIBarButtonItem that you create and set its selector...hope this helps

Daniel
  • 22,363
  • 9
  • 64
  • 71
1

Use isMovingFromParentViewController

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)

    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}
herrkaefer
  • 34
  • 4
  • Could you explain further how this proves the back button was tapped? – Murray Sagal May 04 '17 at 19:13
  • 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. – CMont Nov 19 '17 at 14:54
  • 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. – Phontaine Judd Jan 10 '20 at 03:59
1

Found new way to do it :

Objective-C

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == NULL) {
        NSLog(@"Back Pressed");
    }
}

Swift

override func didMoveToParentViewController(parent: UIViewController?) {
    if parent == nil {
        println("Back Pressed")
    }
}
Ashish Kakkad
  • 23,586
  • 12
  • 103
  • 136
1

The answer from @William is correct however, if the user starts a swipe-to-go-back gesture the viewWillDisappear method is called and even self won't be in the navigation stack (that is, self.navigationController.viewControllers won't contain self), even if the swipe is not completed and the view controller is not actually popped. Thus, the solution would be to:

  1. Disable the swipe-to-go-back gesture in viewDidAppear and only allow using the back button, by using:

    if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)])
    {
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
    
  2. Or simply use viewDidDisappear instead, as follows:

    - (void)viewDidDisappear:(BOOL)animated
    {
        [super viewDidDisappear:animated];
        if (![self.navigationController.viewControllers containsObject:self])
        {
            // back button was pressed or the the swipe-to-go-back gesture was
            // completed. We know this is true because self is no longer
            // in the navigation stack.
        }
    }
    
Rishil Patel
  • 1,977
  • 3
  • 14
  • 30
boherna
  • 659
  • 8
  • 7
1

Swift version of @onegray's answer

protocol RequestsNavigationPopVerification {
    var confirmationTitle: String { get }
    var confirmationMessage: String { get }
}

extension RequestsNavigationPopVerification where Self: UIViewController {
    var confirmationTitle: String {
        return "Go back?"
    }

    var confirmationMessage: String {
        return "Are you sure?"
    }
}

final class NavigationController: UINavigationController {

    func navigationBar(navigationBar: UINavigationBar, shouldPopItem item: UINavigationItem) -> Bool {

        guard let requestsPopConfirm = topViewController as? RequestsNavigationPopVerification else {
            popViewControllerAnimated(true)
            return true
        }

        let alertController = UIAlertController(title: requestsPopConfirm.confirmationTitle, message: requestsPopConfirm.confirmationMessage, preferredStyle: .Alert)

        alertController.addAction(UIAlertAction(title: "Cancel", style: .Cancel) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                let dimmed = navigationBar.subviews.flatMap { $0.alpha < 1 ? $0 : nil }
                UIView.animateWithDuration(0.25) {
                    dimmed.forEach { $0.alpha = 1 }
                }
            })
            return
        })

        alertController.addAction(UIAlertAction(title: "Go back", style: .Default) { _ in
            dispatch_async(dispatch_get_main_queue(), {
                self.popViewControllerAnimated(true)
            })
        })

        presentViewController(alertController, animated: true, completion: nil)

        return false
    }
}

Now in any controller, just conform to RequestsNavigationPopVerification and this behaviour is adopted by default.

Adam Waite
  • 19,175
  • 22
  • 126
  • 148
0

The solution I have found so far is not very nice, but it works for me. Taking this answer, I also check whether I'm popping programmatically or not:

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  if ((self.isMovingFromParentViewController || self.isBeingDismissed)
      && !self.isPoppingProgrammatically) {
    // Do your stuff here
  }
}

You have to add that property to your controller and set it to YES before popping programmatically:

self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];
Community
  • 1
  • 1
Ferran Maylinch
  • 10,919
  • 16
  • 85
  • 100