Mixing PopoverControllers with ModalViewControllers

When building an app on the iPad, sometimes you need to display a Popover, and tapping a button on that Popover will launch a Modal View. When you’re done with the Modal View, it flies away (gotta love those animations), and you’re back to the Popover.

This doesn’t seem like a stretch, but if you’re not careful in how you do it, iOS has some nasty surprises in store for you.

So here’s what you’re trying to accomplish:

Let’s take a first attempt at the code for this:

@implementation ModalPlusPopoverViewController
...
// When the Go Button is pressed, we create a view controller, embed it in a popover controller and present it.
- (IBAction)goPressed:(id)sender {
	MyViewController *theViewController = [[[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil] autorelease];
	UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:theViewController];
	[popoverController setPopoverContentSize:CGSizeMake(400, 400) animated:NO];
	[popoverController presentPopoverFromBarButtonItem:(UIBarButtonItem *)sender
						     permittedArrowDirections:UIPopoverArrowDirectionUp
									       animated:YES];

}
@end
@implementation MyViewController
...
// Inside the popover, when the "Launch Modal" button is pressed, we create our modal view controller and present it
- (IBAction)launchModal:(id)sender {
	MyModalViewController *theModalViewController = [[[MyModalViewController alloc] initWithNibName:@"MyModalViewController" bundle:nil] autorelease];
	theModalViewController.modalPresentationStyle = UIModalPresentationFormSheet;
	[self presentModalViewController:theModalViewController animated:YES];
}
@end
@implementation MyModalViewController
...
// To dismiss the modal, the user taps the Dismiss button, and we dismiss ourselves
- (IBAction)onDismiss:(id)sender {
	[self.parentViewController dismissModalViewControllerAnimated:YES];
}
@end

With this code, everything seems to work well at first glance. When you tap Launch Modal, the modal view slides up from the bottom, and when you tap Dismiss, it slides down again.

But now rotate the iPad and try again. This time, the Modal view slides up from the bottom, but when you dismiss it, it flies out to the left!

And it gets worse. If your modal view happens to be a MFMailComposeViewController, which is a handy class to make it easy for users to send email directly from your app, when you try to dismiss it, it actually causes your app to rotate, even though the iPad is still in landscape orientation.

So what’s going on here?

When you launch and dismiss a modal view from a popover, iOS is doing some strange things under the hood. If you put a breakpoint on shouldAutoRotateToInterfaceOrientation in your popover view controller, you’ll find it gets called when you launch and dismiss the modal. If you examine the call stack you’ll see this call:
[UIViewController _preferredInterfaceOrientationGivenCurrentOrientation:]
So there’s some calculation going on under the hood that’s making an orientation decision. When your modal view is MFMailComposeViewController and you dismiss it, you actually get the wrong orientation passed to shouldAutoRotateToInterfaceOrientation: you get UIInterfaceOrientationPortrait when what you’re expecting is UIInterfaceOrientationLandscapeRight. And this is why your iPad display rotates. And I suspect some similar behaviour is causing the modal view to slide out in the wrong direction.

So how do we fix this?

It turns out our mistake is launching the modal view from the popover. It’s this call from the MyViewController class:
[self presentModalViewController:theModalViewController animated:YES];
Here, self refers to the MyViewController class, which is embedded in a Popover. What we need to do is launch the modal view from the underlying view controller, the one that launched the popover view. In our example, that’s ModalPlusPopoverViewController. The problem is the view controller in the popover has no reference to the underlying controller, so we need to add code for that. Our fixed code looks like this:

@implementation ModalPlusPopoverViewController
...
- (IBAction)goPressed:(id)sender {
	MyViewController *theViewController = [[[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil] autorelease];
       // We now tell the view controller we're embedding in the popover that it should launch modal views from this view controller
	theViewController.baseViewController = self;
	UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:theViewController];
	[popoverController setPopoverContentSize:CGSizeMake(400, 400) animated:NO];
	[popoverController presentPopoverFromBarButtonItem:(UIBarButtonItem *)sender
							  permittedArrowDirections:UIPopoverArrowDirectionUp
											  animated:YES];

}
@end
@implementation MyViewController
@synthesize baseViewController;
...
- (IBAction)launchModal:(id)sender {
	MyModalViewController *theModalViewController = [[[MyModalViewController alloc] initWithNibName:@"MyModalViewController" bundle:nil] autorelease];
	theModalViewController.modalPresentationStyle = UIModalPresentationFormSheet;
	[baseViewController presentModalViewController:theModalViewController animated:YES];
}
@end

I’ve omitted the header file code here, but it should be clear what the header files contain based on the source. Now our modal view behaves as expected when we dismiss it. And if we’re presenting a MFMailComposeViewController, it doesn’t auto-rotate the app behind our backs.

The moral of this story is to be careful which objects you launch your modal views from, especially when you’re launching them from Popovers.

Advertisements