Checking if a UIViewController is about to get Popped from a navigation stack?
Asked Answered
W

15

67

I need to know when my view controller is about to get popped from a nav stack so I can perform an action.

I can't use -viewWillDisappear, because that gets called when the view controller is moved off screen for ANY reason (like a new view controller being pushed on top).

I specifically need to know when the controller is about to be popped itself.

Any ideas would be awesome, thanks in advance.

Wayward answered 13/3, 2009 at 11:46 Comment(1)
Even though this question is 6 years old and answered, you still didn't read the second line in the question where I state "I can't use -viewWillDisappear, because that gets called when the view controller is moved off screen for ANY reason (like a new view controller being pushed on top)."Wayward
N
16

I don't think there is an explicit message for this, but you could subclass the UINavigationController and override - popViewControllerAnimated (although I haven't tried this before myself).

Alternatively, if there are no other references to the view controller, could you add to its - dealloc?

Numberless answered 13/3, 2009 at 12:0 Comment(7)
The dealloc will only be called after the pop, though, not before.Macdermot
I don't think that's the best solution. I want to use this controller in other places in the app, and the behaviour I want to execute is specific to this controller and has to happen when the controller is popped. I don't want to have to subclass every navController this viewController appears in.Wayward
Try this: subclass UIViewController, override popViewController:animated: and send a custom message to the UIViewController's delegate. Then, the delegate can decide what it needs to do in each case.Meander
Subclassing 'UINavigationController' will lead app to be rejected by apple. documentationCalore
Subclass will not get you rejected by apple. Class is just not intended for subclassing because apple uses instances of NSNavigaionController that can't get access too, but there is inherently with subclassing.Ploughman
@Meander UIViewController does not have the popViewController:animated: method.Lepsy
@Wayward On the method viewWillDisappear, One can use self.isMovingFromParent variable to check weather it is being popped or not.Ober
S
92

Override the viewWillDisappear method in the presented VC, then check the isMovingFromParentViewController flag within the override and do specific logic. In my case I'm hiding the navigation controllers toolbar. Still requires that your presented VC understand that it was pushed though so not perfect.

Stereotyped answered 7/3, 2012 at 15:23 Comment(6)
This is a clean solution in iOS 5+, and who isn't on iOS 5 at this point?Airman
From Apple doc. "... For example, a view controller can check if it is disappearing because it was dismissed or popped by asking itself in its viewWillDisappear: method by checking the expression ([self isBeingDismissed] || [self isMovingFromParentViewController])"Attempt
Thanks @Attempt for this comment. I will appreciate if you could add a link to this Apple doc.Reactance
It's actually from inside iOS SDK documentation. You can find this in line 229 to 232 of UIViewController.h as of Xcode 5.1.1.Attempt
Lines have changed to 270-275 as of Xcode 6.1.1 cc: @AttemptInherited
You may need to check isMovingFromParentController and isBeingDismissed both for the current view controller and all parents of the current view controller.Modestia
P
33

Fortunately, by the time the viewWillDisappear method is called, the viewController has already been removed from the stack, so we know the viewController is popping because it's no longer in the self.navigationController.viewControllers

Swift 4

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if let nav = self.navigationController {
        let isPopping = !nav.viewControllers.contains(self)
        if isPopping {
            // popping off nav
        } else {
            // on nav, not popping off (pushing past, being presented over, etc.)
        }
    } else {
        // not on nav at all
    }
}

Original Code

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    if ((self.navigationController) && 
        (![self.navigationController.viewControllers containsObject:self])) {
        NSLog(@"I've been popped!");
    }
}
Pera answered 29/6, 2013 at 19:54 Comment(5)
Definitely the better answer here and one that works at the current time. Removes the need for subclassing, which whilst handy may be a bit over the top for some.Deadline
Calling respondsToSelector is unnecessary. popToRootViewControllerAnimated: is supported by every UINavigationController.Cluj
Also, the predicate test is bad. It only checks if a controller with the same class is in the list, not if this specific controller is there. It would be better to use something simpler like: [self.navigationController.viewControllers containsObject:self]Cluj
Jakob Egger is spot on. I've updated code per his suggestions.Pera
Thanks Caoimhghin (and a fada over the last i to be precise) (pron: kwee-veen) - although I think I might use MattDiPasquale's override as it's a bit simplerAlkalize
R
30

Try overriding willMoveToParentViewController: (instead of viewWillDisappear:) in your custom subclass of UIViewController.

Called just before the view controller is added or removed from a container view controller.

- (void)willMoveToParentViewController:(UIViewController *)parent
{
    [super willMoveToParentViewController:parent];
    if (!parent) {
        // `self` is about to get popped.
    }
}
Rafaelarafaelia answered 25/10, 2013 at 5:4 Comment(5)
Sounds like this is the way to go! Can't wait to try this. +1Nettie
Indeed! why would anyone use viewDidDisappear when it is so much less reliable than willMoveToParentViewController: Using iOS 8.4, I think this should be the accepted answer.Nettie
Another good thing about this method is that view controllers still has reference to the navigation controller (if it has one)Chapeau
Would like to add that this is not as bulletproof as I first thought. Instead of overriding "willMoveToParentViewController", you should override "didMoveToParentViewController" with the same code inside. The reasoning behind this is that "willMoveToParentViewController" will fire-off even if the user did not COMPLETE the pop using the interactive gesture - you will get a false positive; on the other hand, "didMoveToParentViewController" will not fire until the full transition is complete.Allister
also this fires when the controller appears as well as disappearsWimer
N
16

I don't think there is an explicit message for this, but you could subclass the UINavigationController and override - popViewControllerAnimated (although I haven't tried this before myself).

Alternatively, if there are no other references to the view controller, could you add to its - dealloc?

Numberless answered 13/3, 2009 at 12:0 Comment(7)
The dealloc will only be called after the pop, though, not before.Macdermot
I don't think that's the best solution. I want to use this controller in other places in the app, and the behaviour I want to execute is specific to this controller and has to happen when the controller is popped. I don't want to have to subclass every navController this viewController appears in.Wayward
Try this: subclass UIViewController, override popViewController:animated: and send a custom message to the UIViewController's delegate. Then, the delegate can decide what it needs to do in each case.Meander
Subclassing 'UINavigationController' will lead app to be rejected by apple. documentationCalore
Subclass will not get you rejected by apple. Class is just not intended for subclassing because apple uses instances of NSNavigaionController that can't get access too, but there is inherently with subclassing.Ploughman
@Meander UIViewController does not have the popViewController:animated: method.Lepsy
@Wayward On the method viewWillDisappear, One can use self.isMovingFromParent variable to check weather it is being popped or not.Ober
H
14

This is working for me.

- (void)viewDidDisappear:(BOOL)animated
{
    if (self.parentViewController == nil) {
        NSLog(@"viewDidDisappear doesn't have parent so it's been popped");
        //release stuff here
    } else {
        NSLog(@"PersonViewController view just hidden");
    }
}
Hirsh answered 10/2, 2010 at 1:15 Comment(1)
also there's a side effect with full screen uipopovercontrollers or modal view controllers appearing and triggering these.Tapes
R
9

You can catch it here.

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {

    if (viewController == YourAboutToAppearController) {
            // do something
    }
}

This will fire just before the display of the new View. Nobody's moved yet. I use all the time to do magic in front of the asinine NavigationController. You can set titles and button titles and do whatever there.

Rucker answered 3/5, 2009 at 8:18 Comment(2)
My experimentation suggests that actually [UINavigationController visibleViewController] is already set to YourAboutToAppearController. Though indeed the animation has yet to start.Cappella
Using the UINavigationControllerDelegate seems like a better option than subclassing UINavigationController.Granlund
P
3

I have the same problem. I tried with viewDisDisappear, but I don't have the function get called :( (don't know why, maybe because all my VC is UITableViewController). The suggestion of Alex works fine but it fails if your Navigation controller is displayed under the More tab. In this case, all VCs of your nav controllers have the navigationController as UIMoreNavigationController, not the navigation controller you have subclassed, so you will not be notified by the nav when a VC is about to popped.
Finaly, I solved the problem with a category of UINavigationController, just rewrite - (UIViewController *)popViewControllerAnimated:(BOOL)animated

- (UIViewController *)popViewControllerAnimated:(BOOL)animated{
   NSLog(@"UINavigationController(Magic)");
   UIViewController *vc = self.topViewController;
   if ([vc respondsToSelector:@selector(viewControllerWillBePopped)]) {
      [vc performSelector:@selector(viewControllerWillBePopped)];
   }
   NSArray *vcs = self.viewControllers;
   UIViewController *vcc = [vcs objectAtIndex:[vcs count] - 2];
   [self popToViewController:vcc animated:YES];
   return vcc;}

It works well for me :D

Penton answered 18/9, 2010 at 4:18 Comment(3)
This is a great solution and not fragile at all as other suggestions. One could also use a Notification so anyone wanting to know about popped views could listen in.Iphigenia
Yes, this would be a good answer, super fast, without delegate, without notification.... thanks. Adding the logic to the viewDidDisapper is not perfect, for example, when pushing or presenting another view controller inside it, the viewDidDisAppear will be invoked too.... This is why I really like this option.Resolved
Actually, subclass will be a better choice, or there will be a warning, but you can surpress it via: #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation" .......... #pragma clang diagnostic popResolved
E
2

I tried this:

- (void) viewWillDisappear:(BOOL)animated {
    // If we are disappearing because we were removed from navigation stack
    if (self.navigationController == nil) {
        // YOUR CODE HERE
    }

    [super viewWillDisappear:animated];
}

The idea is that at popping, the view controller's navigationController is set to nil. So if the view was to disappear, and it longer has a navigationController, I concluded it was popped. (might not work in other scenarios).

Can't vouch that viewWillDisappear will be called upon popping, as it is not mentioned in the docs. I tried it when the view was top view, and below top view - and it worked in both.

Good luck, Oded.

Ecru answered 31/3, 2011 at 10:24 Comment(3)
An interesting idea and approach, but I fear it may be slightly too fragile. It relies on an implementation detail that could change at any time.Wayward
Agreed, hence that last skepticism.Ecru
Thanks Oded, that little snippet helped quite alot!Barnard
I
2

Subclass UINavigationController and override popViewController:

Swift 3

protocol CanPreventPopProtocol {
    func shouldBePopped() -> Bool
}
  
class MyNavigationController: UINavigationController {
    override func popViewController(animated: Bool) -> UIViewController? {
        let viewController = self.topViewController
        
        if let canPreventPop = viewController as? CanPreventPopProtocol {
            if !canPreventPop.shouldBePopped() {
                return nil
            }
        }
        return super.popViewController(animated: animated)
    }

    //important to prevent UI thread from freezing
    //
    //if popViewController is called by gesture recognizer and prevented by returning nil
    //UI will freeze after calling super.popViewController
    //so that, in order to solve the problem we should not return nil from popViewController
    //we interrupt the call made by gesture recognizer to popViewController through
    //returning false on gestureRecognizerShouldBegin
    //
    //tested on iOS 9.3.2 not others
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        let viewController = self.topViewController
        
        if let canPreventPop = viewController as? CanPreventPopProtocol {
            if !canPreventPop.shouldBePopped() {
                return false
            }
        }
        
        return true
    }

}
Ischia answered 28/1, 2017 at 17:0 Comment(7)
If anyone have freezing issues after using above code, just leave a comment.Ischia
Should I have this issue? I don't see it now, but is it a possible bug?Unrestraint
@RoiMulia I had it during swipe gesture. In iOS 9.3.3. Check if you see that issue during swipe.Ischia
Thanks, I'll check it closelyUnrestraint
It's not freezing, but it's not calling UINavigationController popViewController method.. Did you fixed it?Unrestraint
@RoiMulia Hey, I updated the answer with the code I used. Seems that I had added a comment when I wrote the code.Ischia
Thank you, your answer really helped me, but it did not work right out of box so I have changed it, and will post another answer.Nubile
V
1

You can use this one:

if(self.isMovingToParentViewController)
{
    NSLog(@"Pushed");
}
else
{
    NSLog(@"Popped");
}
Vest answered 3/7, 2015 at 7:51 Comment(0)
N
1

I needed to also prevent from popping sometimes so the best answer for me was written by Orkhan Alikhanov. But it did not work because the delegate was not set, so I made the final version:

import UIKit

class CustomActionsNavigationController: UINavigationController, 
                                         UIGestureRecognizerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        interactivePopGestureRecognizer?.delegate = self
    }

    override func popViewController(animated: Bool) -> UIViewController? {
        if let delegate = topViewController as? CustomActionsNavigationControllerDelegate {
            guard delegate.shouldPop() else { return nil }
        }
        return super.popViewController(animated: animated)
    }

    // important to prevent UI thread from freezing
    //
    // if popViewController is called by gesture recognizer and prevented by returning nil
    // UI will freeze after calling super.popViewController
    // so that, in order to solve the problem we should not return nil from popViewController
    // we interrupt the call made by gesture recognizer to popViewController through
    // returning false on gestureRecognizerShouldBegin
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if let delegate = topViewController as? CustomActionsNavigationControllerDelegate {
            if !delegate.shouldPop() {
                return false
            }
        }

        // This if statement prevents navigation controller to pop when there is only one view controller
        if viewControllers.count == 1 {
            return false
        }

        return true
    }
}

protocol CustomActionsNavigationControllerDelegate {
    func shouldPop() -> Bool
}

UPDATE

I have added viewControllers.count == 1 case, because if there is one controller in the stack and user makes the gesture, it will freeze the UI of your application.

Nubile answered 3/10, 2019 at 7:20 Comment(0)
M
1

You can observe the notification:

- (void)viewDidLoad{
    [super viewDidLoad];
    [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(navigationControllerWillShowViewController:) name:@"UINavigationControllerWillShowViewControllerNotification" object:nil];
}

- (void)navigationControllerDidShowViewController:(NSNotification *)notification{
    UIViewController *lastVisible = notification.userInfo[@"UINavigationControllerLastVisibleViewController"];
    if(lastVisible == self){
        // we are being popped
    }
}
Macrobiotic answered 5/10, 2019 at 22:45 Comment(0)
D
1
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];

    const BOOL removingFromParent = ![self.navigationController.viewControllers containsObject:self.parentViewController];
    if ( removingFromParent ) {
        // cleanup
    }
}
Demonolatry answered 24/10, 2020 at 14:14 Comment(0)
N
0

Maybe you could use UINavigationBarDelegate's navigationBar:shouldPopItem protocol method.

Nominee answered 13/3, 2009 at 20:43 Comment(1)
I tried that first. However, my Navigation Bar is managed by the navigation controller, and manually setting the delegate of the bar to be my view controller results in an exception that explains manually setting the delegate on the nav bar is not allowed if the bar is managed by a nav controller.Wayward
O
0

Try making this check in viewwilldisappear if ([self.navigationController.viewControllers indexOfObject:self] == NSNotFound) { //popping of this view has happend. }

Outhe answered 21/2, 2014 at 8:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.