viewWillDisappear: Determine whether view controller is being popped or is showing a sub-view controller
Asked Answered
N

13

139

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.

Nicholle answered 29/11, 2009 at 20:9 Comment(0)
L
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.

Lathy answered 29/11, 2009 at 20:36 Comment(9)
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 :-)Nicholle
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?Nicholle
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!Nicholle
@Sbrocket is there a reason you didn't do ![viewControllers containsObject:self] instead of [viewControllers indexOfObject:self] == NSNotFound? Style choice?Antifriction
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 UpvoteOphthalmology
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).Alboran
This answer has been obsolete since iOS 5. The -isMovingFromParentViewController method mentioned below allows you to test if the view is being popped explicitly.Polygnotus
For some reason isMovingFromParentViewController is not being called for me. I ended up using if (self.view.superview == nil) and it worked fine.Foah
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?Piliferous
S
142

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)
}
Swap answered 25/6, 2013 at 17:41 Comment(6)
As of iOS 5 this is the answer, maybe also check isBeingDismissedCottontail
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.Donkey
Apple has provided a documented way to do this - https://mcmap.net/q/165807/-viewwilldisappear-determine-whether-view-controller-is-being-popped-or-is-showing-a-sub-view-controllerRemedial
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.Wideeyed
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.Swap
This sometimes doesn't work. I recommend Bryan Henry's answer over this one.Handspike
R
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 { 
    }
}
Remedial answered 2/11, 2015 at 12:57 Comment(0)
C
20

Swift 4

override func viewWillDisappear(_ animated: Bool)
    {
        if self.isMovingFromParent
        {
            //View Controller Popped
        }
        else
        {
            //New view controller pushed
        }
       super.viewWillDisappear(animated)
    }
Counterstroke answered 10/1, 2017 at 10:46 Comment(0)
P
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.)

Probable answered 28/1, 2010 at 17:39 Comment(0)
T
5

In Swift:

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

    super.viewWillDisappear(animated)

}
Tko answered 22/10, 2014 at 0:51 Comment(1)
Make sure to use as! instead of asAlcoholism
M
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")
            }
        }

    }
Metralgia answered 28/8, 2019 at 3:53 Comment(0)
T
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)")
    }
}
Trici answered 11/4, 2017 at 15:49 Comment(0)
M
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
}
Matterhorn answered 4/9, 2013 at 11:23 Comment(0)
O
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");
    }
 }
Otten answered 3/4, 2014 at 19:43 Comment(0)
O
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
    }
}
Orelle answered 4/4, 2014 at 15:50 Comment(0)
D
-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.

Deflective answered 29/11, 2009 at 20:25 Comment(0)
T
-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
Tabular answered 19/6, 2013 at 21:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.