66

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.

Jasarien
  • 58,279
  • 31
  • 157
  • 188
  • 3
    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)." – Jasarien Jul 06 '15 at 08:30

15 Answers15

91

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.

djromero
  • 19,551
  • 4
  • 71
  • 68
Jeff Marino
  • 1,156
  • 1
  • 7
  • 7
  • 4
    This is a clean solution in iOS 5+, and who isn't on iOS 5 at this point? – Ethan Feb 22 '13 at 03:31
  • 16
    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])" – Pei Oct 29 '13 at 15:56
  • Thanks @Pei for this comment. I will appreciate if you could add a link to this Apple doc. – tsafrir Apr 03 '14 at 07:31
  • 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. – Pei Aug 15 '14 at 09:27
  • Lines have changed to 270-275 as of Xcode 6.1.1 cc: @Pei – Ayush Goel Jan 07 '15 at 09:57
  • You may need to check `isMovingFromParentController` and `isBeingDismissed` both for the current view controller and all parents of the current view controller. – Anthony Mills Jul 28 '17 at 16:20
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!");
    }
}
zekel
  • 9,227
  • 10
  • 65
  • 96
caoimghgin
  • 629
  • 1
  • 9
  • 15
  • 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. – Joshua Jul 09 '13 at 17:19
  • Calling `respondsToSelector` is unnecessary. `popToRootViewControllerAnimated:` is supported by every UINavigationController. – Jakob Egger Aug 09 '13 at 09:20
  • 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]` – Jakob Egger Aug 09 '13 at 09:22
  • Jakob Egger is spot on. I've updated code per his suggestions. – caoimghgin Aug 13 '13 at 21:15
  • 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 simpler – amergin Feb 03 '15 at 13:12
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.
    }
}
ma11hew28
  • 121,420
  • 116
  • 450
  • 651
  • Sounds like this is the way to go! Can't wait to try this. +1 – Bamaco Jul 19 '15 at 21:38
  • 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. – Bamaco Jul 20 '15 at 13:37
  • Another good thing about this method is that view controllers still has reference to the navigation controller (if it has one) – ObjSal Feb 02 '16 at 00:58
  • 1
    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. – D6mi Dec 21 '16 at 14:01
  • also this fires when the controller appears as well as disappears – SimonTheDiver Feb 21 '18 at 18:53
15

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?

Tom Elliott
  • 1,908
  • 1
  • 19
  • 39
  • 1
    The dealloc will only be called *after* the pop, though, not before. – Jesse Rusak Mar 13 '09 at 12:03
  • 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. – Jasarien Mar 13 '09 at 13:10
  • 1
    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. – Alex Mar 13 '09 at 20:42
  • 2
    Subclassing 'UINavigationController' will lead app to be rejected by apple. [documentation](http://developer.apple.com/library/ios/#documentation/uikit/reference/UINavigationController_Class/Reference/Reference.html) – HelmiB Nov 15 '11 at 08:00
  • 6
    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. – Zac Bowling Jan 11 '12 at 02:34
  • @Alex UIViewController does not have the popViewController:animated: method. – hariseldon78 Mar 15 '14 at 21:46
  • @Jasarien On the method viewWillDisappear, One can use self.isMovingFromParent variable to check weather it is being popped or not. – Muhammadjon Aug 16 '21 at 18:24
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");
    }
}
mybecks
  • 2,443
  • 8
  • 31
  • 43
Ronald Nepsund
  • 157
  • 1
  • 2
  • 1
    also there's a side effect with full screen uipopovercontrollers or modal view controllers appearing and triggering these. – johndpope Jan 09 '13 at 09:35
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.

Trein
  • 3,658
  • 27
  • 36
dieselmcfadden
  • 203
  • 1
  • 2
  • 1
    My experimentation suggests that actually `[UINavigationController visibleViewController]` is already set to `YourAboutToAppearController`. Though indeed the animation has yet to start. – mxcl Jan 11 '11 at 16:41
  • Using the UINavigationControllerDelegate seems like a better option than subclassing UINavigationController. – Jessedc Apr 24 '13 at 01:15
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

typeoneerror
  • 55,990
  • 32
  • 132
  • 223
hiepnd
  • 821
  • 4
  • 14
  • 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. – David H Nov 02 '11 at 19:49
  • 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. – flypig Nov 17 '12 at 16:52
  • 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 pop – flypig Nov 17 '12 at 16:59
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
    }

}
Community
  • 1
  • 1
Orkhan Alikhanov
  • 9,122
  • 3
  • 39
  • 60
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.

Oded Ben Dov
  • 9,936
  • 6
  • 38
  • 53
1

You can use this one:

if(self.isMovingToParentViewController)
{
    NSLog(@"Pushed");
}
else
{
    NSLog(@"Popped");
}
itechnician
  • 1,645
  • 1
  • 14
  • 24
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.

Simon Moshenko
  • 2,028
  • 1
  • 15
  • 36
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
    }
}
malhal
  • 26,330
  • 7
  • 115
  • 133
1
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];

    const BOOL removingFromParent = ![self.navigationController.viewControllers containsObject:self.parentViewController];
    if ( removingFromParent ) {
        // cleanup
    }
}
0

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

ravoorinandan
  • 783
  • 2
  • 10
  • 26
0

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

François P.
  • 5,166
  • 4
  • 32
  • 31
  • 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. – Jasarien Mar 13 '09 at 20:45