简体   繁体   中英

Confirm back button on UINavigationController

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?

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.

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.

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

If you look at this image:返回按钮屏幕 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à:后退按钮被点击

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

Here's what you need to do to easily make a custom back button that replicates the look of the default back button on iPhone and iPad, with code written out explicitly because I imagine I'll come here looking for this again at some point.

Put the following functions somewhere in the implementation (.m) file of the relevant UIViewController with a UINavigationController, and then in viewDidLoad run [self setupBackButton];

Whatever you'd like to do with the back button, put in the backButtonPressed function.

- (void)setupBackButton {
    UIImage *leftArrowImage;
    UIImage *pressedLeftArrowImage;
    UIButton *customBackButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 48, 30)];
    [customBackButton setAutoresizingMask:UIViewAutoresizingNone];
    customBackButton.titleLabel.font=[UIFont boldSystemFontOfSize:12];
    [customBackButton setTitle:@"Back" forState:UIControlStateNormal];
    [customBackButton setTitleColor:[UIColor lightGrayColor] forState:UIControlStateDisabled];
    [customBackButton setTitleEdgeInsets:UIEdgeInsetsMake(0, 5, 0, 0)];

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        leftArrowImage = [UIImage imageNamed:@"UINavigationBarSilverBack.png"];
        pressedLeftArrowImage = [UIImage imageNamed:@"UINavigationBarSilverBackPressed.png"];
    }
    else {
        leftArrowImage = [UIImage imageNamed:@"UINavigationBarDefaultBack.png"];
        pressedLeftArrowImage = [UIImage imageNamed:@"UINavigationBarDefaultBackPressed.png"];
    }
    UIImage *stretchableLeftArrowImage = [leftArrowImage stretchableImageWithLeftCapWidth:15.0 topCapHeight:0];
    UIImage *stretchablePressedLeftArrowImage = [pressedLeftArrowImage stretchableImageWithLeftCapWidth:15.0 topCapHeight:0];
    [customBackButton setBackgroundColor:[UIColor clearColor]];
    [customBackButton setBackgroundImage:stretchableLeftArrowImage forState:UIControlStateNormal];
    [customBackButton setBackgroundImage:stretchablePressedLeftArrowImage forState:UIControlStateHighlighted];
    [customBackButton addTarget:self action:@selector(backButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
    UIBarButtonItem *aCustomBackButtonItem = [[UIBarButtonItem alloc] initWithCustomView:customBackButton];
    [[self navigationItem] setLeftBarButtonItem:aCustomBackButtonItem];
}

- (void)backButtonPressed:(id)sender {
    NSLog(@"back button pressed");
}

To get the exact button pngs from iOS, I recommend the UIKit Artwork Extractor . After running the project and saving the images on iPad Retina simulator followed by the iPad non-retina simulator, look for the titles in the 'Common' folder in the simulator folder that will appear on your desktop. File names "UINavigationBar...Back(@2x).png" and "UINavigationBar...BackPressed(@2x).png" are what you want.

I'm also attaching the default iOS (iPhone and iPad) back bar button pngs used in the code above as a convenience. Note that with iOS updates, the look of the default back barbuttonitems may change...

Here is a workaround: (Tested on iOS10 and 11)

Add tap gesture recognizer to navigation bar:

let tap = UITapGestureRecognizer(target: self, action: #selector(onBackPressed(gestureRecognizer:)))
tap.cancelsTouchesInView = true
self.navigationController?.navigationBar.addGestureRecognizer(tap)

cancelsTouchesInView = true ensures that the back button does not get the touch event.

Handle the gesture:

@objc func onBackPressed(gestureRecognizer: UITapGestureRecognizer) {
    guard gestureRecognizer.location(in: gestureRecognizer.view).x < 100 else {
        return
    }
    // ... back button is pressed do what you wanted to

Instead of using 100 as magic number you can find the view containing the back button and use it's frame do detect touches on it.

Easy peasy. Just cover the button with a transparent UIControl and capture the touches.

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

#define COVER_HEIGHT    44
    //make this an iVar:  UIControl *backCover;
    if ( backCover == nil ) {
        CGRect cFrame = CGRectMake( 0, self.view.frame.origin.y-COVER_HEIGHT, 100, COVER_HEIGHT);
        backCover = [[UIControl alloc] initWithFrame:cFrame]; // cover the back button
        backCover.backgroundColor = [[UIColor orangeColor] colorWithAlphaComponent:0.2]; // transparent
        // use clearColor later
        [backCover addTarget:self action:@selector(backCoverAction:)
            forControlEvents:UIControlEventTouchDown];
        [self.view.window addSubview:backCover];
    }
}

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

    [backCover removeFromSuperview]; // prevent coverage on another view
    backCover = nil;
}

- (void)backCoverAction:(UIControl *)sender
{
    // decide what to do -- maybe show a dialog.
    // to actually go "Back" do this:
    [self.navigationController popViewControllerAnimated:YES]; // "Back"
}

This scheme also works for tabBar buttons, but it's more complex to determine the location.

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

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM