Confirm back button on UINavigationController

21,146

Solution 1

Unfortunately, you can't intercept the back button in this way. The closest facsimile is to use your own UIBarButtonItem set to the navigationItem.leftBarButtonItem and set an action to display your alert etc. I had a graphic designer create button images that look like the standard back button.

As an aside, I needed to intercept the back button for a different reason. I urge you to reconsider this design choice. If you are presenting a view where users can make changes and you want them to have the choice to save or cancel IMHO it's better to use 'Save' and 'Cancel' buttons vs a back button with an alert. Alerts are generally annoying. Alternatively, make it clear that the changes your users are making are committed at the time they make them. Then the issue is moot.

Solution 2

How I worked around this situation is by setting the leftBarButtonItem to the UIBarButtonSystemItemTrash style (making it instantly obvious that they'll delete the draft item) and adding an alert view confirming the deletion. Because you set a custom leftBarButtonItem it won't behave like a back button, so it won't automatically pop the view!

In code:

- (void)viewDidLoad
{
    // set the left bar button to a nice trash can
    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash
                                                                                          target:self
                                                                                          action:@selector(confirmCancel)];
    [super viewDidLoad];
}

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    if (buttonIndex)
    {
        // The didn't press "no", so pop that view!
        [self.navigationController popViewControllerAnimated:YES];
    }
}

- (void)confirmCancel
{
    // Do whatever confirmation logic you want here, the example is a simple alert view
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Warning"
                                                    message:@"Are you sure you want to delete your draft? This operation cannot be undone."
                                                   delegate:self
                                          cancelButtonTitle:@"No"
                                          otherButtonTitles:@"Yes", nil];
    [alert show];
}

It's really as simple as that! I don't see the big issue, tbh :p

I have to add a disclaimer, though; doing this breaks the default navigation behaviour and apple might not like developers doing this. I haven't submitted any apps (yet) with this feature, so I'm not sure if apple will allow your app in the store when doing this, but be warned ;)

UPDATE: Good news, everybody! In the meanwhile I've released an app (Appcident) to the App Store with this behavior in place and Apple doesn't seem to mind.

Solution 3

Try this solution:

protocol CustomNavigationViewControllerDelegate {
    func shouldPop() -> Bool
}

class CustomNavigationViewController: UINavigationController, UINavigationBarDelegate {
    var backDelegate: CustomNavigationViewControllerDelegate?

    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        return backDelegate?.shouldPop() ?? true
    }
}

class SecondViewController: UIViewController, CustomNavigationViewControllerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()

        (self.navigationController as? CustomNavigationViewController)?.backDelegate = self
    }

    func shouldPop() -> Bool {
        if (needToShowAlert) {
            showExitAlert()
            return false

        } else {
            return true
        }
    }
}

I tested it on iOS 11 and iOS 13 and it works fine :)

Solution 4

Actually, you can find the Back button view and add UITapGestureRecognizer to it.

If you look at this image: Back button screen Using this code:

@interface UIView (debug)
- (NSString *)recursiveDescription;
@end

@implementation newViewController
... 
NSLog(@"%@", [self.navigationController.navigationBar recursiveDescription]);

You can realize how to find the View of the Back button. It is always the last one in subviews array of the navigationbar.

2012-05-11 14:56:32.572 backBtn[65281:f803] <UINavigationBar: 0x6a9e9c0; frame = (0 20; 320 44); clipsToBounds = YES; opaque = NO; autoresize = W; layer = <CALayer: 0x6a9ea30>>
   | <UINavigationBarBackground: 0x6aa1340; frame = (0 0; 320 44); opaque = NO; autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x6aa13b0>>
   | <UINavigationButton: 0x6d6dde0; frame = (267 7; 48 30); opaque = NO; layer = <CALayer: 0x6d6d9f0>>
   |    | <UIImageView: 0x6d70400; frame = (0 0; 48 30); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6d6d7d0>>
   |    | <UIButtonLabel: 0x6d70020; frame = (12 7; 23 15); text = 'Edit'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6d6dec0>>
   | <UINavigationItemView: 0x6d6d3a0; frame = (160 21; 0 0); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6d6d3f0>>
   | <UINavigationItemButtonView: 0x6d6d420; frame = (5 7; 139 30); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x6d6d4e0>>

So I used:

UIView *backButton = [[navBar subviews] lastObject];
[backButton setUserInteractionEnabled:YES];

UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(alertMsg)];
[tapGestureRecognizer setNumberOfTapsRequired:1];
[backButton addGestureRecognizer:tapGestureRecognizer];

Tapping on back button and voilà: Back button was tapped

Solution 5

I figured out an easy solution which works out of the box:

1.) You need to create a custom navigation controller:

//
//  MyNavigationController.swift
//

import UIKit

// Marker protocol for all the VC that requires confirmation on Pop
protocol PopRequiresConfirmation {}

class MyNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

// MARK: - UINavigationBarDelegate Conformance
extension MyNavigationController: UINavigationBarDelegate {
    func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        guard shouldAskConfirmation(for: item) else {
            return true
        }

        self.askUserForConfirmation()
        return false
    }
    
    private func shouldAskConfirmation(for item: UINavigationItem) -> Bool {
        guard
            let vc = self.viewControllers.last(where: { $0.navigationItem === item}),
            vc is PopRequiresConfirmation
        else {
            return false
        }
        
        return true
    }
    
    func askUserForConfirmation() {
        let alertController = UIAlertController(
            title: "Cancel Insertion",
            message: "Do you really want to go back? If you proceed, all the inserted data will be lost.",
            preferredStyle: .alert
        )
        
        alertController.addAction(
            .init(
                title: "Yes, cancel",
                style: .cancel,
                handler: { [weak self] _ in
                    self?.popViewController(animated: true)
                }
            )
        )
        
        alertController.addAction(
            .init(
                title: "No, continue",
                style: .default,
                handler: nil
            )
        )
        
        self.present(alertController, animated: true, completion: nil)
    }
}

2.) Add the following code to all ViewControllers where you need the confirmation "PopRequiresConfirmation":

//
//  ViewController2.swift
//

import UIKit

class ViewController2: UIViewController, PopRequiresConfirmation {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

enter image description here

Share:
21,146
mtmurdock
Author by

mtmurdock

I am currently working for Epic in Madison, Wisconsin. Before that I was doing mobile and web application development in Salt Lake City, UT and attending the University of Utah. I love mobile development because it lets you set aside some of the redundant implementation details and focus on what you're trying to make. People are emotionally attached to their phones, and mobile development gives you the means to reach thousands of users directly. http://www.mtmurdock.com

Updated on October 31, 2021

Comments

  • mtmurdock
    mtmurdock over 2 years

    I am using the storyboard for my app which is using UINavigationController. I would like to add a confirmation dialog to the default back button so the user does not log out accidentally. Is there some way to do this? I have found that I cannot simply access the back button when it has be automatically created by the UINavigationController.

    Any ideas?

  • mtmurdock
    mtmurdock over 12 years
    Well its not a form, its just that if they log out by accident they'll have to reenter their information (security thing). Here's the thing, I'm not adding the back button. Its there by default when using the Storyboard, UIViewController, and segues (which I am). Even if I were doing something like you're describing, that back button would still be there.
  • XJones
    XJones over 12 years
    you can still add a leftBarButtonItem to your storyboard and configure it. It would replace the back button.
  • mtmurdock
    mtmurdock over 12 years
    I hadn't realized that setting leftBarButtonItem replaced the auto button. I now feel silly. Thanks!
  • mtmurdock
    mtmurdock almost 12 years
    Interesting. I have a few questions though. What guarantee do I have that the back button will always be the last object? And does adding a gesture recognizer remove the default behavior, or does it simply add a second action?
  • mafonya
    mafonya almost 12 years
    1. No guarantee :) 2. Adding gesture recognizer omits all other gestures.
  • mtmurdock
    mtmurdock almost 12 years
    Well if there is no guarantee then it isn't a good solution. If Apple changes something in their code to change the order of the buttons in the list then my app will break and I will have a difficult bug to track down.
  • CIFilter
    CIFilter over 11 years
    I've found that UITapGestureRecognizer is pretty much worthless. It doesn't handle the case where a user might tap and hold on a view and then release. Instead, I typically just use a UILongPressGestureRecognizer with the minimumPressDuration property set to 0.0. This effectively handles both tapping and long-pressing a button.
  • Leonard Pauli
    Leonard Pauli about 11 years
    Never do this kind of subview-array-hack! Your app could break with new iOS versions and it is not that easy to understand for other people reading your code. Maybe not a problem now, but later on. Thomas Vervest and @XJones answer is the right way to go.
  • Gabriel.Massana
    Gabriel.Massana almost 10 years
    @XJones yours is the better answer. I replaced the back button with a UIBarButtonSystemItemCancel and I'm poping manually. Also I added a UIBarButtonSystemItemSave as rightBarButtonItem. This is the most clean solution to cancel / replace the back button.
  • Przemysław Wrzesiński
    Przemysław Wrzesiński over 6 years
    And what if the user uses slide-back gesture?
  • Robin Daugherty
    Robin Daugherty about 2 years
    There are other ways to navigate back besides the visible Back button. And covering a button with another breaks accessibility. Please don't do things this way.