UINavigationController and UINavigationBarDelegate.ShouldPopItem() with MonoTouch
Asked Answered
A

5

11

How do I pop up an UIAlertView when the back button of a UINavigationBar (controlled by a UINavigationController) was tapped? Under certain conditions, I want to ask the user an "Are you sure?" type of question so he could either abort the action and stay on the current view or pop the navigation stack and go to the parent view.

The most appealing approach I found was to override ShouldPopItem() on UINavigationBar's Delegate.

Now, there is a quite similar question here: iphone navigationController : wait for uialertview response before to quit the current view

There are also a few other questions of similar nature, for example here: Checking if a UIViewController is about to get Popped from a navigation stack? and How to tell when back button is pressed in a UINavigationControllerStack

All of these state "subclass UINavigationController" as possible answers.

Then there is this one that reads like subclassing UINavigationController is generally not a good idea: Monotouch: UINavigationController, override initWithRootViewController

The apple docs also say that UINavigationController is not intended to be subclassed.

A few others state that overriding ShouldPopItem() is not even possible when using a UINavigationController as that does not allow to assign a custom/subclassed UINavigationBarDelegate to the UINavigationBar.

None of my attempts of subclassing worked, my custom Delegate was not accepted.

I also read somewhere that it might be possible to implement ShouldPopItem() within my custom UINavigationController since it assigns itself as Delegate of its UINavigationBar.

Not much of a surprise, this didn't work. How would a subclass of UINavigationController know of the Methods belonging to UINavigationBarDelegate. It was rejected: "no suitable method found to override". Removing the "override" keyword compiled, but the method is ignored completely (as expected). I think, with Obj-C one could implement several Protocols (similar to Interfaces in C# AFAIK) to achieve that. Unfortunately, UINavigationBarDelegate is not an Interface but a Class in MonoTouch, so that seems impossible.

I'm pretty much lost here. How to override ShouldPopItem() on UINavigationBar's Delegate when it is controlled by a UINavigationController? Or is there any other way to pop up an UIAlertView and wait for it's result before possibly popping the navigation stack?

Adan answered 20/6, 2011 at 15:24 Comment(0)
A
3

For reference, the route I took after giving up on ShouldPopItem() is to replace the back button with a UIBarButtonItem that has a custom UIButton assigned as it's CustomView. The UIButton is crafted to look like the original back button using two images for the normal and the pressed state. Finally, hiding the original back button is required.

Way too much code for what it's supposed to do. So yeah, thanks Apple.

BTW: Another possibility is creating a UIButton with the secret UIButtonType 101 (which is actually the back button) but I avoided this as it may break at any later iOS version.

Adan answered 19/9, 2011 at 6:11 Comment(1)
I'm aware this is crap. However, there didn't seem to be a better way.Adan
B
8

This post is a bit old, but in case you're still interested in a solution (still involves subclassing though):

This implements a "Are you sure you want to Quit?" alert when the back button is pressed, modified from the code here: http://www.hanspinckaers.com/custom-action-on-back-button-uinavigationcontroller/

Turns out if you implement the UINavigationBarDelegate in the CustomNavigationController, you can make use of the shouldPopItem method:


CustomNavigationController.h :

#import <Foundation/Foundation.h>

@interface CustomNavigationController : UINavigationController <UIAlertViewDelegate, UINavigationBarDelegate> {

BOOL alertViewClicked;
BOOL regularPop;
}

@end

CustomNavigationController.m :

#import "CustomNavigationController.h"
#import "SettingsTableController.h"

@implementation CustomNavigationController


- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {

if (regularPop) {
    regularPop = FALSE;
    return YES;
}

if (alertViewClicked) {
    alertViewClicked = FALSE;
    return YES;
}

if ([self.topViewController isMemberOfClass:[SettingsTableViewController class]]) {
    UIAlertView * exitAlert = [[[UIAlertView alloc] initWithTitle:@"Are you sure you want to quit?" message:nil delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Yes", nil] autorelease];

    [exitAlert show];

    return NO;

}   
else {
    regularPop = TRUE;
    [self popViewControllerAnimated:YES];
    return NO;
}   
}

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {
    //Cancel button
}

else if (buttonIndex == 1) {    
        //Yes button
    alertViewClicked = TRUE;
    [self popViewControllerAnimated:YES];
}           
}

@end

The weird logic with the "regularPop" bool is because for some reason just returning "YES" on shouldPopItem only pops the navbar, not the view associated with the navBar - for that to happen you have to directly call popViewControllerAnimated (which then calls shouldPopItem as part of its logic.)

Blithering answered 17/9, 2011 at 9:58 Comment(7)
Thanks for your reply, but I don't think this is applicable with MonoTouch. The problem is (as mentioned in the question) that UINavigationBarDelegate is not a Protocol/Interface but a Class in MonoTouch. The CustomNavigationController would have to inherit from both UINavigationController and UINavigationBarDelegate which is impossible AFAIK.Adan
Thanks, this works perfectly. It took me hours trying to figure out why only the bar would pop and not the view, and indeed adding "regularPop" as you suggested solves the problem.Fracture
I just tried this for the current iOS 7.1 and it still works well.Underbody
Actually I found that if I call popViewControllerAnimated from anywhere else in my code, to manually pop an item off the stack, the presence of this code causes the view to pop but the navigation bar to remain the same (the opposite of the original problem). I had to also override the popViewControllerAnimated method and set regularPop to YES before returning [super popViewControllerAnimated].Underbody
Ditto with needing to override popToViewController if you want to use that method. I haven't tried popToRootViewController, but that might be the same.Underbody
Isn't it simpler just to add a bar button item in place of the "back" button, and add an action outlet to your own method (backButtonClicked) that handles whether they are allowed to go back or not?Valona
It looks like the regularPop trick is no longer needed (and causes the Back button to work incorrectly) in iOS 8.Underbody
M
4

Xamarin does provide the IUINavigationBarDelegate interface to allow you to implement the UINavigationBarDelegate as part of your custom UINavigationController class.

The interface however does not require that the ShouldPopItem method be implemented. All the interface does is add the appropriate Protocol attribute to the class so it can be used as a UINavigationBarDelegate.

So in addition you need to add the ShouldPopItem declaration to the class as follows:

[Export ("navigationBar:shouldPopItem:")]
public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item)
{
}
Medwin answered 30/10, 2014 at 23:26 Comment(2)
Or inherit from UINavigationBarDelegate (The non-interface) version, and override it. Of course this will only work be possible if the class isn't already inheriting from another class.Aideaidedecamp
This is exactly what I was looking for because in my case the NavigationController is "null" and therefore I couldn't set the Delegate. So this is the solution for me. Thanks.Fachan
A
3

For reference, the route I took after giving up on ShouldPopItem() is to replace the back button with a UIBarButtonItem that has a custom UIButton assigned as it's CustomView. The UIButton is crafted to look like the original back button using two images for the normal and the pressed state. Finally, hiding the original back button is required.

Way too much code for what it's supposed to do. So yeah, thanks Apple.

BTW: Another possibility is creating a UIButton with the secret UIButtonType 101 (which is actually the back button) but I avoided this as it may break at any later iOS version.

Adan answered 19/9, 2011 at 6:11 Comment(1)
I'm aware this is crap. However, there didn't seem to be a better way.Adan
A
3

Override only UINavigationBarDelegate methods in a UINavigationController subclass and it should simply work. Be cautious that the protocol methods are also called when you push or pop a view controller from inside your code and not only when the back button is pressed. This is because them are push/pop notifications not button pressed actions.

Astute answered 17/9, 2012 at 13:15 Comment(0)
D
0

I've merged this solution with a native Obj-C solution. This is the way I'm currently handling the cancellation of the BACK button in iOS

It seems that it is possible to handle the shouldPopItem method of the NavigationBar in this way:

  1. Subclass a UINavigationController
  2. Mark your custom UINavigationController with the IUINavigationBarDelegate
  3. Add this method with the Export attribute

    [Export ("navigationBar:shouldPopItem:")] public bool ShouldPopItem (UINavigationBar navigationBar, UINavigationItem item) { }

Now you can handle popping in the ShoulPopItem method. An example to this is to create an interface like this

public interface INavigationBackButton
{
    // This method should return TRUE to cancel the "back operation" or "FALSE" to allow normal back
    bool BackButtonPressed();
}

Then mark your UIViewController which needs to handle the back button with this interface. Implement something like this

public bool BackButtonPressed()
{
    bool needToCancel = // Put your logic here. Remember to return true to CANCEL the back operation (like in Android)
    return needToCancel;
}

Then in your ShouldPopItem Implementation have something like this tanks to: https://github.com/onegray/UIViewController-BackButtonHandler/blob/master/UIViewController%2BBackButtonHandler.m

        [Export("navigationBar:shouldPopItem:")]
        public bool ShouldPopItem(UINavigationBar navigationBar, UINavigationItem item)
        {
            if (this.ViewControllers.Length < this.NavigationBar.Items.Length)
                return true;

            bool shouldPop = true;
            UIViewController controller = this.TopViewController;
            if (controller is INavigationBackButton)
                shouldPop = !((INavigationBackButton)controller).BackButtonPressed();

            if (shouldPop)
            {
                //MonoTouch.CoreFoundation.DispatchQueue.DispatchAsync
                CoreFoundation.DispatchQueue.MainQueue.DispatchAsync(
                    () =>
                    {
                        PopViewController(true);
                    });
            }
            else
            {
                // Workaround for iOS7.1. Thanks to @boliva - http://stackoverflow.com/posts/comments/34452906
                foreach (UIView subview in this.NavigationBar.Subviews)
                {
                    if(subview.Alpha < 1f)
                        UIView.Animate(.25f, () => subview.Alpha = 1);
                }

            }

            return false;                          
        }
Didst answered 13/8, 2015 at 14:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.