Persistant Popover Button in UISplitViewController

August 2nd, 2012 @ 20:24 XCode

This week I've been working on creating a Markdown editor for iOS (iPad and iPhone). Development has been going pretty smoothly up until I went to set the last component of the iPad version of the app, getting the menu button to remain in the upper left corner in portrait view after performing a push.

Usually with XCode, you know if you're going to run into a problem. Generally a quick google search will pull up the right method or attribute that you need to call, but sometimes you just can't find what you're looking for - and usually when that happens, for me at least, I end up spending several days googling and debugging trying to find the right parameter to call. In this case, the one magic word was taggleMasterVisible.

So, this blog post is for both everyone who has spent 4 days looking for an answer for this solution and hasn't been able to find it, and for my own sake in case I ever need to figure this out again. Posted is everything you need to get this to work. Make sure you adjust things to your setup.

First thing, add the following to - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions in AppDelegate.m

    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
        UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
        splitViewController.delegate = (id)navigationController.topViewController;

What this does is sets up your UISplitViewController in your iPadStoryboard. Basic stuff copied straight out of the example MasterDetail app.

Next, we want to add the following to DetailViewController.m

#pragma mark - Split view

- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController
    barButtonItem.title = @"Menu";
    _navBar.leftBarButtonItem = barButtonItem;
    _masterPopoverController = popoverController;

- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
    // Called when the view is shown again in the split view, invalidating the button and popover controller.
    _navBar.leftBarButtonItem = nil;
    _masterPopoverController = nil;

This sets up our detail view controller so that our Menu button appears in the upper left hand corner in portrait mode, and on rotation hides the button and displays the MasterViewController. All that was step one of getting this setup.

Now, the easiest way to transition to another view controller from either the Master or Detail view is to run a pushViewController. That will keep the UINavigationBar in place and present the view. I didn't care for the back button in my app (since it wasn't needed), so what I did to push the next view was:

        UINavigationController *navController = self.navigationController;
        UIStoryboard *storyboard = self.storyboard;
        UIViewController *DetailViewController = [storyboard instantiateViewControllerWithIdentifier:@"DetailViewController"];

        // Pop this controller and replace with another        
        [navController popToRootViewControllerAnimated:FALSE];
        [navController pushViewController:DetailViewController animated:TRUE];

And in the MasterViewController

        [[self.splitViewController.viewControllers lastObject] popToRootViewControllerAnimated:FALSE];
        [[self.splitViewController.viewControllers lastObject] pushViewController:EditorController animated:TRUE];

Which grabs the controller with the identifier DetailViewController from the storyboard, pop to the root controller of the navigationController, and runs an animation to display the next view. In my storyboard.nib file, the DetailViewController identifier is attached navigationController child.

Now, the part that took 4 days to figure out - getting the Menu button to stay there after the push instead of showing a back button.

In DetailViewController method - (void) viewWillAppear

First thing we want to do is hide the back button. Easy.

    _navBar.hidesBackButton = TRUE;

Next, we want to create a new UIBarButtonItem, assign it to be the left bar button item, and run the splitViewController's method, toggleMasterVisible: when clicked.

    UIBarButtonItem *menuButton = [[UIBarButtonItem alloc] initWithTitle:@"Menu" style:UIBarButtonItemStylePlain target:self.splitViewController action:@selector(toggleMasterVisible:)];

Then we set the leftBarButtonItem and enjoy our persistant menu button.

    [_navBar setLeftBarButtonItem:menuButton];

The beauty of this is that the app will automatically hide and reshow the button on rotation automatically without any code changes. The MasterViewController will appear in the same way as it did on the first request on every request that follows.

I'm certain that there is an easier, prettier, better documented, less frustrating way to solve this problem without spending 4 days trying everything else - but this works - and after 4 days I'm not going to go in search of another solution for the sake of looking for something else.

I hope that helps somebody save their time (and more important their sanity). If it does drop me a line, I'd appreciate it.

Oh, and if you received this via Facebook or Twitter earlier this morning I apologize for that. I accidentally published the draft at the same time that TwitterFeed picked up the RSS Feed (like what I did there? Bet you didn't even know CiiMS had a built in RSS feed! That will work for all categories (eg and the main blog page (