iPad Modal View Controllers

There were some minor tweaks to modal view controllers with iPhone OS 3.2 that are worth taking a look at if you need a modal view for an iPad or universal app.

For changes introduced in iOS 5 and iOS 6 including the deprecation of presentModalViewController:animated: see also this more recent post: Presenting View Controllers

Modal view controllers are well documented in the View Controller Programming Guide for iPhone OS but here is a quick recap of how to present and dismiss a modal view. To keep things simple I will cover the steps for presenting the modal view when a button is pressed on a master view controller. When the button is pressed we need to create and show a detail view controller that contains a button that the user can use to dismiss the modal view controller. The NIB file for the detail view controller is trivial in this example consisting of just a single view containing a text label.

Allocating and showing the detail view controller is straightforward:

- (void)buttonAction:(id)sender {

  // Create the modal view controller
  DetailViewController *viewController = [[DetailViewController alloc]
                        initWithNibName:@"DetailViewController"
                        bundle:nil];

  // We are the delegate responsible for dismissing the modal view 
  viewController.delegate = self;

  // Create a Navigation controller
  UINavigationController *navController = [[UINavigationController alloc]
                          initWithRootViewController:viewController];

  // show the navigation controller modally
  [self presentModalViewController:navController animated:YES];

  // Clean up resources
  [navController release];
  [viewController release];
}

After allocating and initializing the controller from its NIB file the delegate (which I will explain shortly) is set to the current master view controller. The important step is that we do not display the detail view controller directly. We actually create a navigation bar controller and initialize it with the detail view controller. This provides a convenient navigation bar to hold a button for dismissing the view. Once the navigation bar is configured correctly it is displayed with the presentModalViewController method of UIViewController.

The detail view controller needs to add a “Done” button to the navigation bar which will trigger the master view controller to dismiss the modal view. The viewDidLoad method is the best place to add the button:

- (void)viewDidLoad {
  [super viewDidLoad];

  // Override the right button to show a Done button
  // which is used to dismiss the modal view
  self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc]
                      initWithBarButtonSystemItem:UIBarButtonSystemItemDone 
                      target:self
                      action:@selector(dismissView:)] autorelease];
}

The target action of the button is the method dismissView in the detail view controller. Best practise is that the view controller that created the modal view should also dismiss it. We could do this by adding an instance variable to the detail view controller of type MasterViewController and initialize it to contain a reference to the master view controller when we first create the modal view. However this makes the DetailViewController directly dependent on the master view controller which is a pain if we later want to use this modal view from elsewhere in the application.

A better solution is to make use of the delegate protocol pattern. We define a protocol in the detailViewController that all callers of the modal view must implement. The protocol ensures that all delegates of our class must implement the didDismissModalView method:

@protocol ModalViewControllerDelegate <NSObject>
- (void)didDismissModalView;
@end

The interface for the DetailViewController contains a weak reference to the delegate as follows:

@interface DetailViewController : UIViewController {
  id<ModalViewControllerDelegate> delegate;
}

@property (nonatomic, assign) id<ModalViewControllerDelegate> delegate;

You should also remember to @synthesize delegate in the implementation of DetailViewController. We can now implement the dismissView method in the DetailViewController:

// Done button clicked
- (void)dismissView:(id)sender {

  // Call the delegate to dismiss the modal view
  [delegate didDismissModalView];
}

This illustrates the advantage of delegation. The DetailViewController does not now need to know who the calling controller is it only has to call the didDissmisModalView method. The compiler ensures that this method exists in the calling controller. The MasterViewController must declare that it implements the delegation protocol:

#import "DetailViewController.h"

@interface MasterViewController : UIViewController
                                 <ModalViewControllerDelegate> {
  UIButton *button;
}

@property (nonatomic,retain) IBOutlet UIButton *button;
- (IBAction)buttonAction:(id)sender;
@end

In this simple example the implementation of the didDismissModalViewmethod is trivial:

- (void)didDismissModalView {
  // Dismiss the modal view controller
  [self dismissModalViewControllerAnimated:YES];
}

In more complex situations this method might validate changes to an object made by the user in the modal view prior to updating the model. The Apple View Controller Programming Guide nicely sums up the advantages of this approach:

  • The delegate object has the opportunity to validate or incorporate changes from the modal view controller before that view controller is dismissed.
  • The use of a delegate promotes better encapsulation because the modal view controller does not have to know anything about the parent object that presented it. This enables you to reuse that modal view controller in other parts of your application.

Prior to iPhone OS 3.2 the modal view filled the whole screen which on an iPhone sized device is probably the only option. However with the larger screen size of an iPad there are some more options to control the appearance of the modal view. There is a new view controller property named modalPresentationStyle which allows you to select the required style when creating the view controller:

  • UIModalPresentationFullScreen: This is the default style and as the name suggests causes the modal view to be presented full screen exactly as with the iPhone
  • UIModalPresentationPageSheet: The modal view occupies the full screen height but the width is set to the width of the screen in portrait mode. This means that when the device is in portrait mode it still occupies the full screen. However in landscape mode it leaves some of the underlying master view to still be visible, but dimmed as the user cannot interact with the uncovered master view controller.
  • UIModalPresentationFormSheet: Both the width and height of the modal view are set to be smaller than the screen size. The modal view is centered on the screen and the uncovered areas of the master view controller are dimmed to prevent user interaction. If the modal view makes used of the keyboard the modal view is moved upwards to make room.
  • UIModalPresentationCurrentContext: The modal view is displayed using the same style as the parent master view controller.

The differences between the Page and Form sheet styles in landscape mode are easier to see than explain:

Using these new styles can make for a better user experience when what you have to display cannot usefully take advantage of the larger full screen space on the iPad. Showing the detailed content in a smaller than full screen window with the rest of the interface dimmed can help the user focus on the modal view. Note that on an iPhone or iPod Touch the modal view controller is always displayed full screen. To use one of the new presentation styles set the property on the controller you are presenting modally. In our previous example this would be the navigation controller in the buttonAction method of the master view controller:

navController.modalPresentationStyle = UIModalPresentationFormSheet;

There are some limitations on when you can use these new styles. If you are using the UIModalTransitionStylePartialCurl style for example you must use the full screen presentation style or you will get an exception.

One final change introduced in iPhone OS 3.2 that impacts modal view controllers is their use with popovers. A popover is generally expected to be dismissed when the user touches outside of the popover. Of course if you are displaying a modal view controller in the popover this behaviour is probably not what you want. To prevent this you can set the modalInPopover boolean for the view controller. With the flag set the popover will not be dismissed by touches outside of the popover.