Receiving local notifications with UITabBarController

I spent some time on trial-and-error last week to fix a bug in my app. I thought the details of what I'd done wrong and what the fix was might be useful for others.

I've been working on a new branch of my iOS companion app for Exist, where I'm building out the Today dashboard view. (You can read about this in more depth on the Hello Code blog.)

As part of this change, I've added a UITabBarController, with each tab holding a navigation controller.

The problem was that launching the app via a local notification caused it to crash. The notification is supposed to create and push a view controller, ReportViewController, that's normally only accessed by tapping on cells within the main view controller, MoodListViewController (which is now one of three view controllers inside the tab bar controller). My old code in the delegate's didReceiveLocalNotification method looked like this:

// Create report view
ReportViewController *reportVC = (ReportViewController *)
[self.window.rootViewController.storyboard
    instantiateViewControllerWithIdentifier:@"reportVC"];

// Show report view
[self.window.rootViewController showViewController:reportVC 
    sender:self];

In my old version, self.window.rootViewController was the MoodListViewController, so this code worked reliably. The problem in the new version is that self.window.rootViewController is now a tab bar controller. This caused two problems:

The tab bar controller couldn't show my ReportViewController. I needed to get to my MoodListViewController so its navigation controller could push the report view.

Secondly, the tab bar wasn't created in a storyboard, but to create the ReportViewController, which was created in a storyboard, I'm looking for self.window.rootViewController.storyboard. Since the MoodListController is no longer the root view controller, this method doesn't return me a storyboard, and thus my report view was never being initialised.

What I ended up doing to create the report view was this:

// Create report view
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" 
    bundle:nil];
ReportViewController *reportVC = [storyboard 
    instantiateViewControllerWithIdentifier:@"reportVC"];

Retrieving my storyboard with storyboardWithName is much more reliable, since I know the ReportViewController exists in this storyboard, and I'm no longer making assumptions about what the root view controller is (or whether its storyboard is the one I need).

To show the report view once it's instantiated with the data it needs, I'm doing this:

// Show report vc
UITabBarController *tabBarController = (UITabBarController *)
    self.window.rootViewController;
[tabBarController setSelectedIndex:1]; // This is the mood list

// Each VC's nav controller is added to the tabbar
//not the VC itself so I need to access the nav controller
// via the tabbar to open a new report vc
UINavigationController *moodNav = (UINavigationController *)
    tabBarController.selectedViewController;
[moodNav pushViewController:reportVC animated:NO];

This is probably not the best way to do it, since I'm choosing an item from the tab bar controller using an index and then assuming (which is right now, but won't necessarily always be) that it's the mood list's nav controller. If I ever change the order of the tabs, this will be a problem.

From the research I've done, it seems like using the index to choose which view controller to select is the most common approach. And since the tab bar's view controllers are accessible as an array, I can't use a key to get the right one. So for now I don't know a better way to approach this, but I've fixed my bug. Phew!