Popping Tabbed Navigation Controllers

A common scenario I am seeing when using local notifications is to return the user to the root view of a navigation view hierarchy when a local notification arrives. This is particularly useful when the app may be suspended in the background for some time. When the user clicks to view the notification I want the app to return to the foreground with the context reset to the root view regardless of what the user was last doing in the app. In situations where the app has a tab bar this may also mean changing which tab bar item is currently selected to move the user to the correct tab.

Tabbed Navigation

Combining navigation controllers with tab bars is a common user interface pattern on the iPhone. The sample TabNav application I created to illustrate adding iAds to an application uses this approach. For the purposes of this discussion I have modified this app to allow a local notification to be scheduled by pushing a button in the detailed view controller. This view controller is accessed by selecting a row in the table displayed by the root view controller of the first tab bar item:

I will not discuss the details of scheduling the notification as I covered that previously instead I will focus on how we can manipulate the view hierarchy when the notification is delivered. As it stands when the notification arrives the user is returned to the view they were last using when they sent the app to the background. What I want to achieve is for the user to be returned to the root view of the first (“Top Rated”) tab bar item.

Pop to root

The UINavigationController class contains a number of instance methods to allow manipulation of the stack of controllers in the navigation hierarchy. As the user navigates down and back up a view hierarchy the methods pushViewController:animated: and popViewControllerAnimated: are called to push and pop viewControllers onto the stack managed by the navigation controller. However in a situation where you just want to get back to the root view controller regardless of the current position you can call popToRootViewControllerAnimated: directly:

[myNavController popToRootViewControllerAnimated:YES];

This method actually returns an array containing the viewControllers (if any) that were popped from the stack should you need to perform additional actions (maybe to release resources) on those views.

Selecting Tab Bar Items

As well as moving the user back to the root of a view hierarchy it is also necessary to make sure they are actually on the correct tab for applications that use a tab bar. The UITabBarController class contains an instance variable conveniently named viewControllers which is an array of the root view controllers displayed by the tab bar. There are two ways to select a view controller in this array and make the corresponding tab bar item active.

The selectedIndex property of a UITabBarController can be used to directly set which tab bar item is selected. A value of zero corresponds to the left most tab bar item. In simple cases setting the value of selectedIndex can be sufficient. For example if you know that always want to select the left most tab bar item. However if you are using more than five tab bar items the special More navigation controller can make life more complicated. By default the user can rearrange the view controllers and select which ones are visible on the tab bar. In this situation it can be easier to set the selectedViewController property rather than trying to keep track of the index for individual view controllers.

Putting it all together

Determining that a local notification has arrived is achieved by implementing the application:didReceiveLocalNotification: method in the application delegate. For a first attempt I will make the assumption that we always want to return to the first tab bar item so we can simply set the selectedIndex of the tab bar controller to zero:

- (void)application:(UIApplication *)application
        didReceiveLocalNotification:(UILocalNotification *)notification {
  
  tabBarController.selectedIndex = 0;
  UINavigationController *firstNavController =
        (UINavigationController *)[tabBarController selectedViewController];
  [firstNavController popToRootViewControllerAnimated:YES];
}

After setting the selected tab bar controller we then use the selectedViewController method to retrieve a reference to the navigation controller which then has its view stack popped to get back to the root view controller.

This approach will work fine but I do not like it as it will break if we ever rearrange our tab bar or in situations where the user is allowed to rearrange the tab bar. The approach that I actually prefer is to uniquely tag each of the tab bar items when I create the tab bar. Then when required I can search the viewControllers array for the view controller that I want without having to make any assumptions about the order of tab bar.

For reference the view hierarchy in Interface Builder looks like this:

Each navigation controller contains a tab bar item which is tagged. For this example the Top Rated tab bar item is tagged with “100” and the History tab bar item with “101”:

Our method to handle local notifications then becomes as follows:

- (void)application:(UIApplication *)application
        didReceiveLocalNotification:(UILocalNotification *)notification {

  UINavigationController *firstNavController = nil;
  for (UIViewController *viewController in tabBarController.viewControllers) {
    
    if (viewController.tabBarItem.tag == kTabBarItemTagTopRated) {
      firstNavController = (UINavigationController *)viewController;
      break;
    }
  }

  if (firstNavController) {
    tabBarController.selectedViewController = firstNavController;
    [firstNavController popToRootViewControllerAnimated:YES];
  }	
}

First we search the viewControllers array of the tab bar controller for the navigation controller which has a tab bar item with the tag we set for the Top Rated tab. Once we have a reference to the correct navigation controller we use it to set the currently selected view controller for the tab bar and then to pop the navigation controller hierarchy back to the root view controller.