138

I'm struggling to find a good solution to this problem. In a view controller's -viewWillDisappear: method, I need to find a way to determine whether it is because a view controller is being pushed onto the navigation controller's stack, or whether it is because the view controller is disappearing because it has been popped.

At the moment I'm setting flags such as isShowingChildViewController but it's getting fairly complicated. The only way I think I can detect it is in the -dealloc method.

TheNeil
  • 3,321
  • 2
  • 27
  • 52
Michael Waterfall
  • 20,497
  • 27
  • 111
  • 168

13 Answers13

234

You can use the following.

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];
  NSArray *viewControllers = self.navigationController.viewControllers;
  if (viewControllers.count > 1 && [viewControllers objectAtIndex:viewControllers.count-2] == self) {
    // View is disappearing because a new view controller was pushed onto the stack
    NSLog(@"New view controller was pushed");
  } else if ([viewControllers indexOfObject:self] == NSNotFound) {
    // View is disappearing because it was popped from the stack
    NSLog(@"View controller was popped");
  }
}

This is, of course, possible because the UINavigationController's view controller stack (exposed through the viewControllers property) has been updated by the time that viewWillDisappear is called.

Community
  • 1
  • 1
Bryan Henry
  • 8,348
  • 3
  • 34
  • 30
  • 2
    Perfect! I don't know why I didn't think of that! I guess I didn't think the stack would be altered until the disappear methods had been called! Thanks :-) – Michael Waterfall Nov 29 '09 at 21:56
  • 1
    I've just been trying to perform the same thing but in `viewWillAppear` and it would seem that whether the view controller is being revealed by it being pushed or something above it being popped, The viewControllers array is the same both ways! Any ideas? – Michael Waterfall Nov 30 '09 at 11:49
  • I should also note that the view controller is persistent through the app's lifetime so I can't perform my actions on `viewDidLoad` as it's only called once! Hmm, tricky one! – Michael Waterfall Nov 30 '09 at 11:56
  • 4
    @Sbrocket is there a reason you didn't do `![viewControllers containsObject:self]` instead of `[viewControllers indexOfObject:self] == NSNotFound`? Style choice? – zekel Feb 20 '12 at 19:32
  • for me doesn't show either of those messages in NSLog, I have investigated why: NSLog(@"controllers count: %d", viewControllers.count); it shows 1, thats why.. :) but is a good solution Upvote –  Aug 09 '12 at 09:10
  • If you are dealing with a view controller managed by a tab bar controller it will still be in `self.navigiationController.viewControllers`. I found it easier to check whether the controller had presented another view controller: `if (self.presentedViewController == nil)`. – pr1001 Mar 14 '13 at 13:13
  • 25
    This answer has been obsolete since iOS 5. The `-isMovingFromParentViewController` method mentioned below allows you to test if the view is being popped explicitly. – grahamparks Oct 01 '13 at 11:20
  • For some reason isMovingFromParentViewController is not being called for me. I ended up using if (self.view.superview == nil) and it worked fine. – arsenius Jun 25 '15 at 02:51
  • I wrote a test extention in Swift. Pop is working; however, wasPushed is NOT. I call it in `viewDidAppear` `func wasPushed() -> Bool { assert(navigationController != nil, "There needs to be a navigation controller!") let viewControllers = navigationController!.viewControllers return (viewControllers.count > 1 && (viewControllers[viewControllers.count - 2] == self)) }` Any ideas? – oyalhi Mar 11 '16 at 06:24
141

I think the easiest way is:

 - (void)viewWillDisappear:(BOOL)animated
{
    if ([self isMovingFromParentViewController])
    {
        NSLog(@"View controller was popped");
    }
    else
    {
        NSLog(@"New view controller was pushed");
    }
    [super viewWillDisappear:animated];
}

Swift:

override func viewWillDisappear(animated: Bool)
{
    if isMovingFromParent
    {
        print("View controller was popped")
    }
    else
    {
        print("New view controller was pushed")
    }
    super.viewWillDisappear(animated)
}
Andrea Gottardo
  • 1,329
  • 11
  • 23
RTasche
  • 2,614
  • 1
  • 15
  • 19
  • As of iOS 5 this is the answer, maybe also check isBeingDismissed – d370urn3ur Dec 23 '13 at 10:52
  • 4
    For iOS7 I have to check [self.navigationController.viewControllers indexOfObject:self] == NSNotFound again because backgrounding the app will also pass this test but won't remove self from navigation stack. – Eric Chen Feb 28 '14 at 10:07
  • 3
    Apple has provided a documented way to do this - http://stackoverflow.com/a/33478133/385708 – Shyam Bhat Nov 02 '15 at 12:59
  • The problem with using viewWillDisappear is that it is possible that the controller is popped from the stack while the view is already disappeared. For example, another viewcontroller could be pushed on top of the stack and then call popToRootViewControllerAnimated bypassing viewWillDisappear on the ones in the middle. – John K Dec 23 '16 at 00:38
  • Suppose you have two controllers (root vc and another pushed) on your navigation stack. When the third one is being pushed viewWillDisappear is called on the second whose view is going to disappear, right? So when you pop to root view controller (pop the third and second) viewWillDisappear is called on the third i.e. last vc on the stack because it's view is on top and is going to disappear at this time and second's view already had disappeared. That's why this method is called viewWillDisappear and not viewControllerWillBePopped. – RTasche Dec 23 '16 at 09:28
  • This sometimes doesn't work. I recommend Bryan Henry's answer over this one. – coolcool1994 Apr 22 '17 at 22:29
64

From Apple's Documentation in UIViewController.h :

"These four methods can be used in a view controller's appearance callbacks to determine if it is being presented, dismissed, or added or removed as a child view controller. 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])."

- (BOOL)isBeingPresented NS_AVAILABLE_IOS(5_0);

- (BOOL)isBeingDismissed NS_AVAILABLE_IOS(5_0);

- (BOOL)isMovingToParentViewController NS_AVAILABLE_IOS(5_0);

- (BOOL)isMovingFromParentViewController NS_AVAILABLE_IOS(5_0);

So yes, the only documented way to do this is the following way :

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    if ([self isBeingDismissed] || [self isMovingFromParentViewController]) {
    }
}

Swift 3 version:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    if self.isBeingDismissed || self.isMovingFromParentViewController { 
    }
}
Community
  • 1
  • 1
Shyam Bhat
  • 1,600
  • 13
  • 22
20

Swift 4

override func viewWillDisappear(_ animated: Bool)
    {
        if self.isMovingFromParent
        {
            //View Controller Popped
        }
        else
        {
            //New view controller pushed
        }
       super.viewWillDisappear(animated)
    }
brainray
  • 12,512
  • 11
  • 67
  • 116
Umair
  • 1,203
  • 13
  • 15
18

If you just want to know whether your view is getting popped, I just discovered that self.navigationController is nil in viewDidDisappear, when it is removed from the stack of controllers. So that's a simple alternative test.

(This I discover after trying all sorts of other contortions. I'm surprised there's no navigation controller protocol to register a view controller to be notified on pops. You can't use UINavigationControllerDelegate because that actually does real display work.)

n00bProgrammer
  • 4,261
  • 3
  • 32
  • 60
dk.
  • 2,030
  • 1
  • 22
  • 22
5

In Swift:

 override func viewWillDisappear(animated: Bool) {
    if let navigationController = self.navigationController {
        if !contains(navigationController.viewControllers as! Array<UIViewController>, self) {
        }
    }

    super.viewWillDisappear(animated)

}
user754905
  • 1,799
  • 3
  • 21
  • 29
2

Thanks @Bryan Henry, Still works in Swift 5

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if let controllers = navigationController?.children{
            if controllers.count > 1, controllers[controllers.count - 2] == self{
                // View is disappearing because a new view controller was pushed onto the stack
                print("New view controller was pushed")
            }
            else if controllers.firstIndex(of: self) == nil{
                // View is disappearing because it was popped from the stack
                print("View controller was popped")
            }
        }

    }
dengST30
  • 3,643
  • 24
  • 25
1

I find Apple's documentation on this is hard to understand. This extension helps see the states at each navigation.

extension UIViewController {
    public func printTransitionStates() {
        print("isBeingPresented=\(isBeingPresented)")
        print("isBeingDismissed=\(isBeingDismissed)")
        print("isMovingToParentViewController=\(isMovingToParentViewController)")
        print("isMovingFromParentViewController=\(isMovingFromParentViewController)")
    }
}
Norman
  • 3,020
  • 22
  • 21
0

This question is fairly old but I saw it by accident so I want to post best practice (afaik)

you can just do

if([self.navigationController.viewControllers indexOfObject:self]==NSNotFound)
 // view controller popped
}
0

This applies to iOS7, no idea if it applies to any other ones. From what I know, in viewDidDisappear the view already has been popped. Which means when you query self.navigationController.viewControllers you will get a nil. So just check if that is nil.

TL;DR

 - (void)viewDidDisappear:(BOOL)animated
 {
    [super viewDidDisappear:animated];
    if (self.navigationController.viewControllers == nil) {
        // It has been popped!
        NSLog(@"Popped and Gone");
    }
 }
Byte
  • 2,920
  • 3
  • 33
  • 55
0

Segues can be a very effective way of handling this problem in iOS 6+. If you have given the particular segue an identifier in Interface Builder you can check for it in prepareForSegue.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"LoginSegue"]) {
       NSLog(@"Push");
       // Do something specific here, or set a BOOL indicating
       // a push has occurred that will be checked later
    }
}
Kyle Clegg
  • 38,547
  • 26
  • 130
  • 141
-1

Here is a category to accomplish the same thing as sbrocket's answer:

Header:

#import <UIKit/UIKit.h>

@interface UIViewController (isBeingPopped)

- (BOOL) isBeingPopped;

@end

Source:

#import "UIViewController+isBeingPopped.h"

@implementation UIViewController (isBeingPopped)

- (BOOL) isBeingPopped {
    NSArray *viewControllers = self.navigationController.viewControllers;
    if (viewControllers.count > 1 && [viewControllers objectAtIndex:viewControllers.count-2] == self) {
        return NO;
    } else if ([viewControllers indexOfObject:self] == NSNotFound) {
        return YES;
    }
    return NO;
}

@end
bbrame
  • 18,031
  • 10
  • 35
  • 52
-1

I assume you mean that your view is being moved down the navigation controller's stack by the pushing a new view when you say pushed onto the stack. I would suggest using the viewDidUnload method to add a NSLog statement to write something to the console so you can see what is going on, you may want to add a NSLog to viewWillDissappeer.

Aaron
  • 252
  • 3
  • 16