49

I currently need to implement some code when the top view controller is being popped off from my navigation controller. Is there a way to detect when the view controller is being popped off the navigation controller stack?

As much as possible I want to stay away from using viewWillDisappear or viewDidDisappear because I'm using a splitview in my project, and selecting a different row in the master view will also trigger the viewWillDisappear/viewDidDisappear methods.

aresz
  • 2,589
  • 6
  • 34
  • 51
  • Why not use the navigation controller's delegate method? – matt Feb 20 '14 at 17:31
  • 2
    You can still use viewWillDissapear/viewDidDisappear and find out whether the viewcontroller was really popped. This might help: http://stackoverflow.com/questions/1816614/viewwilldisappear-determine-whether-view-controller-is-being-popped-or-is-showi – Niraj Feb 20 '14 at 18:10

5 Answers5

111

You can detect whether a view is being popped using the isMovingFromParentViewController property for a view controller as shown below:

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

isMovingFromParentViewController

Returns a Boolean value that indicates that the view controller is in the process of being removed from its parent.

Nishant
  • 12,529
  • 9
  • 59
  • 94
  • @Nishant I understand that isMovingFromParentViewController is called when the view disappears, but the view can disappear because the ViewController was pushed or popped. Is this method `isMovingFromParentViewController` called too when the viewController is pushed on the navigation stack or only when it is poped (e.g back button)? – bibscy Feb 21 '17 at 21:07
  • @bibscy `isMovingFromParentViewController` is only called when the view controller is popped, not pushed. – Mike Taverne Aug 15 '17 at 06:38
  • 1
    How to handle the "popToRootViewController" case ? – subin272 Aug 01 '18 at 00:17
25

UPDATE 2015-04-30

Based on phatmann's feedback (first comment below), I was curious if something had changed since I answer this question over a year ago. I put together a simple, example app, and have some results that are interesting.

Option 1, example

https://github.com/greymouser/TestNVC

I don't have the ability to easily test pre-8.x, so I'm not sure if something has changed since then. However, the behavior I originally described does still happen. However, thanks to puting together the test app, I did notice an oddity I didn't before.

If I just rely on {will,did}MoveToParentViewController, I noticed a spurious didMoveToParentViewController: call when pushing the first non-rootVC, on the rootVC, with parent != nil (implying it is added, not being removed). I didn't encounter this around the time of my original answer, as I usually have "permanent" rootVC's on my NVC's, and hadn't implemented the callbacks there. See the example app with logging set to LOG_WILL_DID_MTPVC (in ViewController.m). This is an -- edited for space -- snapshot of what I saw:

TestNVC[] -[vc(rootVC) willMoveToParentViewController [entering]
TestNVC[] -[vc(rootVC) didMoveToParentViewController [entering]
TestNVC[] -[vc(1) willMoveToParentViewController [entering]
TestNVC[] -[vc(rootVC) didMoveToParentViewController [entering]  # <-- this is odd
TestNVC[] -[vc(1) didMoveToParentViewController [entering]
...

My original answer suggested using {will,did}MoveToParentViewController alone, as it was a "one stop shop" to handle this behavior. However, now that I've seen the spurious call to the rootVC, I suggest a mix of {will,did}MoveToParentViewController as well as the standard UINavigationControllerDelegate callbacks. For this behavior in the example app, set logging to LOG_WILL_DID_MTPVC_LEAVING_AND_NVC_WILL_DID_SHOW_VC. Now we see the following:

TestNVC[] -[nvcD willShowViewController]: rootVC
TestNVC[] -[nvcD didShowViewController]: rootVC
TestNVC[] -[nvcD willShowViewController]: 1
TestNVC[] -[nvcD didShowViewController]: 1
TestNVC[] -[nvcD willShowViewController]: 2
TestNVC[] -[nvcD didShowViewController]: 2
TestNVC[] -[vc(2) willMoveToParentViewController [leaving]
TestNVC[] -[nvcD willShowViewController]: 1
TestNVC[] -[vc(2) didMoveToParentViewController [leaving]
TestNVC[] -[nvcD didShowViewController]: 1
TestNVC[] -[vc(1) willMoveToParentViewController [leaving]
TestNVC[] -[nvcD willShowViewController]: rootVC
TestNVC[] -[vc(1) didMoveToParentViewController [leaving]
TestNVC[] -[nvcD didShowViewController]: rootVC

... and this makes much more sense now.

Option 2

Another option I didn't explore is using your NVC sublcass, overriding - pushViewController:animated: and - popViewControllerAnimated:, and applying whatever behaviors you want to the VC being pushed, or the VC that was returned from the pop. (Make sure to remember to call super in your overrides if you attempt this.)

Update summary

So, thanks to phatmann for the chance to readdress this. I think my answer is more correct now. However, I'm not so sure that it was ever "fully non-truthy". ;-)

ORIGINAL

If the exact behavior you described is what you are looking for, then override the following on your child view controller:

- (void)willMoveToParentViewController:(UIViewController *)parent;
- (void)didMoveToParentViewController:(UIViewController *)parent;

willMoveToParentViewController: will get called with parent != nil when entering, and parent == nil when leaving. didMoveToParentViewController: will always have parent != nil.

Sometimes, viewDidDisappear may make sense. However, if you're truly looking for push and pop from the parent container view controller, those methods above are what you want.

LondonGuy
  • 10,778
  • 11
  • 79
  • 151
greymouser
  • 3,133
  • 19
  • 22
  • At least in iOS 8, `willMoveToParentViewController` is not called for child view controllers in a navigation controller. And `didMoveToParentViewController` is called when the child view controller is initially placed in the navigation view controller, in addition to being called when popped to. So these methods are useless for what the OP wanted. No choice but to downvote. – phatmann Apr 29 '15 at 21:59
  • @phatmann Thanks for the feedback. I've updated my answer above with an example app and logging, too. Your comment helped me to identify an issue I didn't notice with respect to my first answer, however, I don't think your concerns/points are 100% correct either. Please see the update for full explanation. Cheers! – greymouser Apr 30 '15 at 14:04
  • Wow, great research, thanks for doing this. Now upvoted :-) However, Apple makes those calls weird enough that I will continue to avoid them. Best to use a boolean, gross as that is. – phatmann May 05 '15 at 02:31
2

For Swift Users (Swift 3 - 4.2):

I wanted to detect when the view controller is being popped from the stack, so I wasn't able to use viewWillDisappear or viewDidDisappear callbacks, since these callbacks will be called when the view controller is not visible anymore, and not when it's popped from the stack.

but you can use the navigation controller Delegates UINavigationControllerDelegate by doing the below:

let your controller conform to UINavigationControllerDelegate:

class ViewController : UIViewController {

      override func viewDidLoad() {
          super.viewDidLoad()
          self.navigationController?.delegate = self
      }

}




extension ViewController : UINavigationControllerDelegate {
     
      override func willMove(toParent parent: UIViewController?) {
     
     /*You can detect here when the viewcontroller is being popped*/
      
     }

}

hope this helps, goodluck

johndpope
  • 5,035
  • 2
  • 41
  • 43
MhmdRizk
  • 1,591
  • 2
  • 18
  • 34
1

If you don't need to know before the view controller is removed, and just need to know it has been popped, you can also use deinit.

class ViewController: UIViewController {

    deinit {
        // View controller has been popped/dismissed and it's being released
    }
}

This method works well to notify coordinators or other delegates.

Eneko Alonso
  • 18,884
  • 9
  • 62
  • 84
  • This can work too but take note that deinit might not be called if you are subscribed to a NSNotificationCenter event or if the view controller is holding onto a timer, etc.. Best to use other methods described in other answers to be safe. – CyberMew Sep 11 '19 at 01:12
  • You don't know when the system will actually deinit VC. For example in default master -> detail scenario, detail VC will only be deinited when the system will try to open a new VC of that class. Meaning that after pop, the VC still won't be deinited. It will only happen on next push. – inokey Mar 23 '20 at 10:02
  • I've never seen this behavior. Are you sure you are not retaining the view controller, @inokey? – Eneko Alonso Mar 23 '20 at 16:38
  • @EnekoAlonso it's funny you should ask because this approach actually helped me to debug this. Initially I had a reference issue in the class and VC was never released. But I'm pretty sure right now that it's being released but only after it was recreated. Still may be there IS some sort of weak reference, that holds VC longer than it should. I wasn't trying to tell that your approach is totally invalid but it may not be suitable for some setups, I guess. – inokey Mar 23 '20 at 16:49
0

My experience with iOS 13 is that the property value of isMovingFromParent is not always consistent. When you have search controller being in active mode (search text field is tapped), back to parent view will have false value for this property.

Here is my way to determine if a view is from parent or not:

class MyBaseViewController: UIViewController {
    private var _isPushedToAnotherView = false
    var isPushedToAnotherView: Bool {
      return _isPushedToAnotherView
    }
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
      super.prepare(for: segue, sender: sender)
      ...
      _isPushedToAnotherView = true
    }
    override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
      ...
      _isPushedToAnotherView = false
    }
    ...
}

class MyExtendedClass: MyBaseViewController {
  ...
  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    ...
    if !isPushedToAnotherView {
        // clear resources hold by this class
    }
}
David.Chu.ca
  • 37,408
  • 63
  • 148
  • 190