Presenting View Controllers

It has been a long time since I posted about about iPad Modal View Controllers. The last couple of years has seen the introduction first of iOS 5 and storyboards and now iOS 6. This has in turn seen the old methods for presenting and dismissing modal views controllers discouraged and now formally deprecated. So I think it is time for a quick update on what has changed and how to migrate old code.

In the good old days of iOS 3.2 and iOS 4 if you wanted to create a modal view controller you would do so with code that looked something like this:

DetailViewController *viewController = [[DetailViewController alloc]
  initWithNibName:@"DetailViewController" bundle:nil];
viewController.delegate = self;
UINavigationController *navController = [[UINavigationController alloc]
  initWithRootViewController:viewController];
[self presentModalViewController:navController animated:YES];

This code fragment is taking from the old iOS 3.2 era post on iPad Modal View Controllers but is fairly typical. The user interface XIB file is first created by Interface Builder and then used to instantiate the new view controller.

In this example the view controller is embedded in a navigation controller but the key to presenting the modal view controller is the call to presentModalViewController:animated:. A similar call to dismissModalViewControllerAnimated: in the presenting view controller takes care of removing the modal view controller.

The modalPresentationStyle property controls whether the view controller is presented full screen, as a page or form sheet on the iPad. Likewise the modalTransitionStyle property controls how the new view animates onscreen.

Presented and Presenting View Controllers

With iOS 5 Apple started to deprecate the modal view controller methods and replace them with the more general concept of presenting and presented view controllers. The modal view controller being the presented view controller. Conceptually not much has changed and if you adopt storyboards it gets even easier. Consider a trivial two view controller application represented by the following storyboard:

storyboard

The root view contains three buttons each of which is wired to trigger a different modal segue to the presented view controller. The three segues have unique identifiers and specify both the modal presentation and transition styles in the storyboard. For example here is the storyboard segue for the full screen button:

full screen segue

The page sheet segue which is full screen height but uses the portrait screen width when in landscape is specified as follows:

page sheet segue

Finally the form sheet segue which uses both a view height and width smaller than full screen is specified as follows:

form sheet segue

If you are not using storyboards and creating a separate NIB file for the presented view controller you will need to implement a method for the various UIButton actions that creates and presents the new controller manually. For example, the method triggered when the full screen button is tapped would look something like this:

- (IBAction)selectedFullScreen:(UIButton *)sender {
  UYLPresentedViewController *viewController = [[UYLPresentedViewController alloc]
  initWithNibName:@"UYLPresentedViewController" bundle:nil];
  viewController.delegate = self;
  viewController.modalPresentationStyle = UIModalPresentationFullScreen;
  [self presentViewController:viewController animated:YES completion:NULL];
}

Note the call to presentViewController:animated:completion:. The completion argument can take a block which is called after the viewDidAppear: method of the presented view controller. Specify NULL if you do not need the completion handler.

Dismissing Presented View Controllers

Not much has changed when it comes to dismissing modal view controllers other than the name of the method you should use. Best practise suggests that you arrange for the presenting view controller to dismiss the presented view controller which generally means setting up a delegate protocol. This is nothing new but for completeness I will show a typical interface for a presented view controller:

@protocol UYLPresentedViewControllerDelegate <NSObject>
- (void)didDismissPresentedViewController;
@end

@interface UYLPresentedViewController : UIViewController
@property (nonatomic, weak) id<UYLPresentedViewControllerDelegate> delegate;
@end

The UYLPresentedViewController class defines a protocol that we expect all users of the class to adopt. It has a single mandatory method that will be called when we determine that the presented view controller will be dismissed. If you need to pass data back to the presenting view controller you could add an argument to this method. The public interface definition of the class includes a weak reference to the delegate.

The done button in the presented view controller is then wired to call the following action method when it is touched:

- (IBAction)didSelectDone:(UIButton *)sender {
  [self.delegate didDismissPresentedViewController];
}

The presenting view controller needs to adopt the protocol:

@interface UYLViewController : UIViewController <UYLPresentedViewControllerDelegate>

It also needs to set itself as the delegate which is not something we can do directly in the storyboard. Instead we need to implement prepareForSegue:sender: as follows to retrieve the newly instantiated view controller and set the delegate:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
  if ([segue.identifier isEqualToString:UYLSegueFullScreen] ||
      [segue.identifier isEqualToString:UYLSeguePageSheet] ||
      [segue.identifier isEqualToString:UYLSegueFormSheet]) {
    UYLPresentedViewController *viewController = segue.destinationViewController;
    viewController.delegate = self;
  }
}

With the delegate set we can complete the implementation of didDismissPresentedViewController.

- (void)didDismissPresentedViewController {
  [self dismissViewControllerAnimated:YES completion:NULL];
}

Note that we now call dismissViewControllerAnimated:completion: instead of dismissModalViewControllerAnimated:. The completion handler in this case if specified is called after the viewDidDisappear: method is called on the presented view controller.

If defining the delegate protocol seems like a lot of work for such a trivial example you can take a short cut. The UIViewController class now has two properties to indicate both the presentedViewController and the presentingViewController. We could use this to dismiss the view controller directly from the presented view controller. For example we could change our UIButton action method to the following:

- (IBAction)didSelectDone:(UIButton *)sender {
  [self.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
}

This achieves the same effect without the need to define and set a delegate but in general I prefer the fuller approach of keeping the presentation and dismissal all in the presenting view controller. The use of delegation also makes it easier when you need to pass data back to the presenting view controller.

Migration

As of the time of writing this post the presentModalViewController:animated: and dismissModalViewControllerAnimated: methods do still exist in iOS 6 but are deprecated and will generate Xcode compilation warnings. If you are starting a new project today and you can target iOS 5 or later you should just use the new methods. Likewise if you have an existing app that uses the deprecated methods now is a good time to think about updating assuming you no longer require iOS 4.

If you have already made the switch to using storyboards for the user interface you may find that you have already eliminated the calls to presentModalViewController:animated:. As I showed above the storyboard takes care of instantiating and presenting the view controller for you.

However since with storyboards you still need to take care of dismissing the view controller you may have some calls to dismissModalViewControllerAnimated: buried in your view controller code. The migration in this case is trivial since you can generally replace these with dismissViewControllerAnimated: as I described above.

Updated: 10-Oct-2012

As was pointed out in the comments the code as originally posted for dismissing the presented (modal) view controller from within the presented view controller contained a mistake. The dismissViewControllerAnimated:completion: method should of course be called on self.presentingViewController. A further comment pointed out some unnecessary code in the didSelectDone method that was checking the value of self.delegate which is unnecessary since a method call on nil is harmless. The code snippets in the post have been updated accordingly.