Wednesday, April 14, 2010

UIPopoverController and SplitView with iPad

[Update: This is the most popular post on this blog by about a factor of 100. If you wouldn't mind leaving a comment as to what you were looking for as you arrived I would appreciate it. I am happy to modify and expand this tutorial to make it more useful. -wt]

I have been working a lot on the Massage Helper application recently and think that I'm almost done. Something that I really struggled with was ensuring that popover views were getting dismissed correctly. Apple's guidelines require that only one UIPopoverController is visible at any time. For most popovers this is a relatively straightforward process. We just need to remember to set a variable in our view to hold the currently visible popover and each time we want to display a new popover we need to dismiss any visible popover's first.

In these examples, I'm using the template that Apple provides when we start a project as a split-view project. If you want to follow along, simply create a new project in XCode of type SplitView and then just build and go. In landscape mode, you will see the split view and in portrait mode, the smaller of the views will become a popover. It is this popover that we are talking about. The sample template has a RootViewController (the smaller view that becomes the popover) and a DetailViewController (the bigger view that is always visible).

The sample template has a property associated with the DetailViewController called "popoverController". Whenever a popover is displayed, this property gets to point to it. If the popoverController property is set to nil then no popover is being displayed. We have added, a little function called "managePopovers" that checks the property and if a popover is being displayed is gets dismissed. We will call managePopovers each time we are about to display a new popover. This code is lifted from the "setDetailItem:" function of the DetailViewController.m. Here are the examples.

managePopovers

if (popoverController != nil) {

[popoverController dismissPopoverAnimated:YES];

}


Displaying one of my popovers

-(IBAction)updateSessionDate:(id)sender{

DateChangeViewController* myDateChange = [[DateChangeViewController alloc] init];

UIPopoverController *tempPopover = [[UIPopoverController alloc] initWithContentViewController:myDateChange];


[self managePopovers];

self.popoverController = tempPopover;

tempPopover.delegate = self;

[myDateChange release];

[tempPopover presentPopoverFromBarButtonItem:sender permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];

}


The DateChangeViewController referenced above does all of its setup without a .nib. The key to managing the popovers is that we call managePopovers and then set the popoverController property of the view equal to the popover we are about to display.

Sidenote: One other thing I discovered is that Popovers don't seem to like .nib files very much. When I would try to create a view from a .nib and use it in a popover, I tended to get errors about key value compliance. I haven't tried to explore if it's me or the computer, I just started to create any view that needs to be in a popover programatically.

So, simple. However, there is one popover that doesn't quite work well and that is the popover associated with the split view itself (in the Apple template it is the popover of the list of locations). The SplitView template that Apple provides only cares about the one popover it needs to function as a demonstration. So, when the split view sets itself up it sets the popoverController property but then it doesn't set the popoverController property on subsequent times the SplitView popover displays. Popovers will dismiss themselves whenever the user taps outside of their bounds, so if you only have one popover, you never need to dismiss it. This is what the Apple template relies on. The issue with that is that tapping on a UIBarButtonItem to display another popover doesn't trigger the event to dismiss a popover. So, using the code above we can get the Apple supplied popover to dismiss the first time we display our DateChange popover, but the next time we display the Apple supplied popover we can't get it to dismiss.

Thankfully, the key to fixing this is to implement one of the methods that Apple provides for SplitView controllers and their delegates. There are actually 3 methods a SplitView controller can support, but the Apple template only uses two of them. The willPresentViewController method gives us the opportunity to set the popoverController property each time the SplitView controller's popover is about to display.

Here is my implementation of the method (this is in the DetailViewController.m):

- (void)splitViewController:(UISplitViewController*)svc popoverController:(UIPopoverController*)pc willPresentViewController:(UIViewController *)aViewController{

[self managePopovers];

self.popoverController = pc;

}

Now, every time the popover associated with the SplitView itself displays, the method is called and the popoverController property gets set.

This was a maddening part of working with the UIPopoverController class, so I hope that you stumble across this little hint and that it saves you some time. If anyone at Apple is reading, it would be great if you added the willPresentViewController code to the SplitView template and then commented it out like you do for the UITableView and other templates.

8 comments:

Ferdinand G Rios said...

Thank you for this article. I have been looking for some code that will allow me to KEEP the split view in portrait mode as does the SETTINGS app.

Apple's docs say that it is possible, but I have not been able to figure out or find an example of how to do this. I bet its probably something simple i am missing. Any ideas?

Walter said...

It is simple but tread with caution as it's a private API call and your app is likely to be rejected in the App store. I placed this line of code in my AppDelegate's applicationDidFinishLaunching method
[splitViewController setHidesMasterViewInPortrait:NO];

So, what's a programmer to do? You can create your own custom view controller with a UITable view and a normal UIView and then manage the rotation by hand.

Walter said...

I played with this some more just now. I think that the best way to do what you want is probably to just pretend that you have a Split View (i.e. don't use a UISplitView) and just use subviews. I made a quick mock up in IB and just hooked my two views to IBOutlets. It seemed to function ok. You can make a UITableView be a subview of your view and you can make a UIImageView or a UIView be the subview of your view. As before, you will have to manage rotation and resizing by hand.

Jeff said...

Excellent post - told me exactly what I needed to know (the :willPresentViewController delegate method). Apple docs didn't make very clear how that really functions...

Dylan said...

You asked why I landed here. I'm looking for a solution to the bug in the template where the popover button doesn't appear if you quickly rotate from landscape to portrait after starting app or rotate portrait 180.

Dylan said...

It's worth noting that Apple recommends against using self.popoverController for custom controllers.

http://developer.apple.com/iphone/library/qa/qa2010/qa1694.html

Walter said...

@Dylan I saw the bug you mentioned once on my simulator but haven't yet been quick enough to make it happen on my device. Thanks for the link to the Apple boards. For all of the popoverControllers except for the one that Apple supplies by default, we can get at them by name so there is no need to use self.popoverController.

Mk said...

Thanks for your tutrial, just looking for some iPad UIPopoverController code :)

Post a Comment