Setting action for back button in navigation controller

IosIphoneCocoa TouchUinavigationcontrollerUibarbuttonitem

Ios Problem Overview


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

Ios Solutions


Solution 1 - Ios

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

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

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

Solution 4 - Ios

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

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.

Solution 6 - Ios

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.

Solution 7 - Ios

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

}

Solution 8 - Ios

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

Solution 9 - Ios

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;

Solution 10 - Ios

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

Solution 11 - Ios

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

Solution 12 - Ios

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;

Solution 13 - Ios

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

Solution 14 - Ios

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

Solution 15 - Ios

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."

Solution 16 - Ios

Using Swift:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    if self.navigationController?.topViewController != self {
        print("back button tapped")
    }
}

Solution 17 - Ios

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

Solution 18 - Ios

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 {

    }
}

Solution 19 - Ios

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.

Solution 20 - Ios

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

Solution 21 - Ios

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.

Solution 22 - Ios

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

Solution 23 - Ios

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

Solution 24 - Ios

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
}

Solution 25 - Ios

Use isMovingFromParentViewController

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(true)
    
    if self.isMovingFromParentViewController {
        // current viewController is removed from parent
        // do some work
    }
}

Solution 26 - Ios

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

Solution 27 - Ios

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

Solution 28 - Ios

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.

Solution 29 - Ios

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionParrotsView Question on Stackoverflow
Solution 1 - IosWilliam JockuschView Answer on Stackoverflow
Solution 2 - IosonegrayView Answer on Stackoverflow
Solution 3 - IosHansPinckaersView Answer on Stackoverflow
Solution 4 - IoskgaidisView Answer on Stackoverflow
Solution 5 - IosAlex ReynoldsView Answer on Stackoverflow
Solution 6 - IospsychoView Answer on Stackoverflow
Solution 7 - IosSiempayView Answer on Stackoverflow
Solution 8 - IosJason MooreView Answer on Stackoverflow
Solution 9 - IosZar E AhmerView Answer on Stackoverflow
Solution 10 - IosAutomatonTecView Answer on Stackoverflow
Solution 11 - IosSarasrangltView Answer on Stackoverflow
Solution 12 - IosMeltemiView Answer on Stackoverflow
Solution 13 - Iosjazzyjef2002View Answer on Stackoverflow
Solution 14 - IosIvanView Answer on Stackoverflow
Solution 15 - Iosuser2612791View Answer on Stackoverflow
Solution 16 - IosMurray SagalView Answer on Stackoverflow
Solution 17 - IosLawlietView Answer on Stackoverflow
Solution 18 - IosEdward L.View Answer on Stackoverflow
Solution 19 - IosTahoeWolverineView Answer on Stackoverflow
Solution 20 - IosDanielView Answer on Stackoverflow
Solution 21 - IosTravis M.View Answer on Stackoverflow
Solution 22 - IosJeffView Answer on Stackoverflow
Solution 23 - IosDan LoughneyView Answer on Stackoverflow
Solution 24 - IoszonoView Answer on Stackoverflow
Solution 25 - IosherrkaeferView Answer on Stackoverflow
Solution 26 - IosAshish KakkadView Answer on Stackoverflow
Solution 27 - IosbohernaView Answer on Stackoverflow
Solution 28 - IosAdam WaiteView Answer on Stackoverflow
Solution 29 - IosFerran MaylinchView Answer on Stackoverflow