March 11, 2009

Trapping the UINavigationBar Back Button (part 2)

After posting the first part of this article, it was pointed out to me that there is an alternative solution: handle the viewWillDisappear message. This is certainly a more elegant solution, but it does have its drawbacks, which is why I did not use it in the first place. However, it is a valid solution in some cases and can be of use to some people, so I will describe it here. There is a set of messages that get sent to your UIViewController subclass during various events:
- (void)viewWillDisappear:(BOOL)animated - (void)viewDidDisappear:(BOOL)animated - (void)viewWillAppear:(BOOL)animated - (void)viewDidAppear:(BOOL)animated
These are called for the event described in the method name. For example, the viewWillDisappear is called just before the view disappears. Now for my situation, I needed to trap the event of my controller being popped off. The problem with using viewWillDisappear (or viewDidDisappear) is that it is called in different scenarios. When a controller is pushed on top of yours, the viewWillDisappear method is called. And when your controller is popped off, the method is called again. The problem for me is the event is too general. Your view is disappearing, but the reason why it's disappearing is not provided. In my situation, my controller can push other controllers, but I only wanted to know when I was popped off, not pushed on top of. In order to differentiate between the two scenarios, I need additional intelligence in my code to determine which situation I am in. Basically, whenever I am about to push on a new controller, I have to set a flag to ignore the next viewWillDisappear message. Then I need to handle the viewWillAppear message to clear this flag. Not a big deal, but seems like a hassle to handle something fairly basic. Here is the framework needed to support this:
// ---------------------------------------------------------
// YourController.h
@interface YourController : UIViewController
 BOOL ignoreDisappear;

// ---------------------------------------------------------
// YourController.m

// this helper method will push a new controller onto the navigation stack
- (void) myPushController:(UIViewController*) newController
  // first, set our flag to ignore the next viewWillDisappear message
  ignoreDisappear = YES;

  // now do the push
  [self.navigationController pushViewController:newController animate:YES];
  [newController release];

- (void) viewWillDisappear:(BOOL) animate
  // do we care about this event?
  if(ignoreDisappear == NO) {
     // this is your back button handle logic

  [super viewWillDisappear:animate];

- (void) viewWillAppear:(BOOL) animate
  // clear the flag
  ignoreDisappear = NO;

  [super viewWillAppear:animate];
But that was my situation. If your controller is at the end of the UINavigationBar hiearchy, meaning you cannot have another controller pushed onto yours, then you are safe assuming that the viewWillDisappear is being called for the scenario where your controller is being popped off. Take caution, though. Should you later change the behavior of your code to allow additional controllers to be pushed, your viewWillDisappear will get invoked in what may be an inappropriate time. One other thing I should mention. There is this warning in the Apple reference in each of the four methods listed above:
Warning: If the view belonging to a view controller is added to a view hierarchy directly, the view controller will not receive this message. If you insert or add a view to the view hierarchy, and it has a view controller, you should send the associated view controller this message directly. Failing to send the view controller this message will prevent any associated animation from being displayed.
I'm not real clear on what the phrase "added to a view hierarchy directly" means, but there are numerous reported problems related to this. I don't have these problems myself, but they are reported here and here.


  1. The following looks at the navigation stack to determine if the view was pushed or popped:

    - (void) viewDidDisappear:(BOOL)animated
    NSLog(@"just pushed = %d", [self.navigationController.viewControllers containsObject:self]);