PopViewController strange behaviour

2019-02-28 22:31发布

Due to a weird request which I tried to turn down but it didn't work, I had to override the navigationBar's back Button.

I have made a custom UINavigationController subclass and hacked the - (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item method.

Here is my code:

@interface CustomUINavigationController ()

@end

@implementation CustomUINavigationController


#pragma mark - UINavigationBar delegate methods

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

    if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
        ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
        [vc1 handleBackAction];
        if (vc1.canPopVC == YES) { 
            [self popViewControllerAnimated:YES];
            return YES;
        } else {
            return NO;
        }
    }

    [self popViewControllerAnimated:YES];
    return YES;
}

@end

All works fine, except when I pop a viewController programmatically. The app crashed every time when I wanted to perform a push after said pop. Turning NSZombie on, revealed that when popping a viewController programmatically, its parent viewController is deallocated. At this point, making a custom backButton is not a option since it will lose the native iOS 7 swipe to popViewController feature.

Crash log:

*** -[ContactsDetailViewController performSelector:withObject:withObject:]: message sent to deallocated instance 0x1806b790

4条回答
祖国的老花朵
2楼-- · 2019-02-28 22:50

I'm not 100% certain but I don't think you should actually be popping the view controller in that delegate method.

"should" delegate methods don't normally do something. They just assert whether something should or shouldn't be done.

Change your method to this...

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

    if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
        ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
        [vc1 handleBackAction];
        if (vc1.canPopVC == YES) { 
            return YES;
        } else {
            return NO;
        }
    }

    return YES;
}

And see if it works.

All I have done is removed the popViewController calls.

EDIT - How to add a custom back button

In a category on UIBarButtonItem...

+ (UIBarButtonItem *)customBackButtonWithTarget:(id)target action:(@SEL)action
{
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button setBackgroundImage:[UIImage imageNamed:@"Some image"] forState:UIControlStateNormal];
    [button setTitle:@"Some Title" forState:UIControlStateNormal];
    [button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];

    UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];

    return barButtonItem;
}

Now whenever you want to set a custom back button just use...

UIBarButtonItem *backButton = [UIBarButtonItem customBackButtonWithTarget:self action:@selector(backButtonPressed)];
查看更多
倾城 Initia
3楼-- · 2019-02-28 22:50

I would suggest a completely different approach.

Create a base class for the view controllers that you are pushing on the navigation stack. In the viewDidLoad method set your custom button as the leftBarButtonItem of the navigationItem and add a -backAction: which invokes the popViewControllerAnimated: method of the navigation controller.

That way you won't care about things like losing functionality of UINavigationController like the swipe to pop and you won't have to override the navigationBar:shouldPopItem: method at all.

查看更多
Fickle 薄情
4楼-- · 2019-02-28 23:08

You probably need to do [super shouldPop... instead of actual [self popViewControllerAnimated:YES];.

The reason being that the way UINavigationController implements stack is private, so you should mess with the method calls as little as possible.

Anyway, this looks like a hack. Moreover, the user will have no visual clue that you are blocking the navigation action. What's wrong with disabling the button via:

self.navigationController.navigationItem.backBarButtonItem.enabled = NO; 
查看更多
疯言疯语
5楼-- · 2019-02-28 23:10

(My previous post was completely wrong. This is a complete rewrite with an appropriate solution.)

I had this behavior pop up when I chose to delete some code generating a warning when I was converting to ARC -- code that I thought was not being called.

Here's the situation:

If you shadow navigationBar:shouldPopItem: in a subclass of UINavigationController, then the current view controller will NOT be popped when the user touches the NavBar's BACK button. However, if you call popViewControllerAnimated: directly, your navigationBar:shouldPopItem: will still be called, and the view controller will pop.

Here's why the view controller fails to pop when the user touches the BACK button:

UINavigationController has a hidden method called navigationBar:shouldPopItem:. This method IS called when the user clicks the BACK button, and it is the method that normally calls popViewControllerAnimated: when the user touches the BACK button.

When you shadow navigationBar:shouldPopItem:, the super class' implementation is not called, and hence the ViewController is not popped.

Why you should NOT call popViewControllerAnimated: within your subclass' navigationBar:shouldPopItem::

If you call popViewControllerAnimated: within navigationBar:shouldPopItem:, you will see the behavior that you desire when you click the BACK button on the NavBar: You can determine whether or not you want to pop, and your view controller pops if you want it to.

But, if you call popViewControllerAnimated: directly, you will end up popping two view controllers: One from your direct call to popViewControllerAnimated:, and one from the call you added to within navigationBar:shouldPopItem:.

What I believe to be the safe solution:

Your custom nav controller should be declared like this:

@interface CustomNavigationController : UINavigationController <UINavigationBarDelegate> 
{
    // .. any ivars you want
}
@end

Your implementation should contain code that looks something like this:

// Required to prevent a warning for the call [super navigationBar:navigationBar shouldPopItem:item]
@interface UINavigationController () <UINavigationBarDelegate>
@end


@implementation CustomNavigationController

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
    BOOL rv = TRUE;

    if ( /* some condition to determine should NOT pop */ )
    {
        // we won't pop
        rv = FALSE;

        // extra code you might want to execute ...
    } else
    {
        // It's not documented that the super implements this method, so we're being safe
        if ([[CustomNavigationController superclass]
                instancesRespondToSelector:@selector(navigationBar:shouldPopItem:)])
        {
            // Allow the super class to do its thing, which includes popping the view controller 
            rv = [super navigationBar:navigationBar shouldPopItem:item];

        }
    }

    return rv;
}
查看更多
登录 后发表回答