UIPresentationController is (currently) a deficient API

iOS 8 brings a new and welcome class to UIKit: UIPresentationController, which reifies the presentation of view controllers in a configurable object whose lifetime can also be used to more cleanly manage auxiliary UI elements associated with that presentation.

Conceptually, popovers are a form of presentation, so they are now managed via a subclass of UIPresentationController, logically named UIPopoverPresentationController. Unfortunately, the actual API design surrounding this class leaves a lot to be desired.

Updated: When I wrote this blog post, the documentation for UIPopoverPresentationController was out of date, and described a version of the API that was changed before iOS 8.0 beta 1 was released. In the final SDK, -popoverPresentationController returns an object that can be configured before the actual presentation, as one would expect. Thanks to those who pointed out the incongruity.

There are still issues with the current API: the inability to use a particular subclass of UIPresentationController and the awkward delegate-based customization API are among them. These issues, and the subsequent suggestions about re-building UINavigationController on top of UIPresentationController, are still relevant. If you’d like, you can skip to the relevant bits.

The intended use case is to configure a view controller’s modalPresentationStyle to UIModalPresentationPopover and then present it using -presentViewController:… on another view controller. As part of the presentation process, UIKit will then create a UIPopoverPresentationController instance, which the presented view controller can access via its popoverPresentationController property.

Note that this implies that the popover presentation controller does not exist until after the call to -presentViewController:…. As such, the popover can’t be configured until it’s already been presented!

The clearest warning comes straight from the introduction to the UIPopoverPresentationController class reference:

Configuring the popover presentation controller after calling presentViewController:animated:completion: might seem counter-intuitive but UIKit does not create a presentation controller until after you initiate a presentation. In addition, UIKit must wait until the next update cycle to display new content onscreen anyway. That delay gives you time to configure the presentation controller for your popover.

I understand that there are lots of well-designed APIs out there that might be considered “counter-intuitive”. And some are even counter-intuitive in similar ways: Core Animation, for example, has some common pitfalls that arise from its transactional nature.

But to my knowledge, there are no iOS or OS X APIs that so violate the fundamental conceit of imperative programming languages—that the side effects of code happen in the same order as the statements that cause them—that they require a paragraph of warning at the top of their documentation. Nor would any well-designed API need to excuse the confusion by exposing implementation details such as its use of the runloop.

Aside from just being plain confusing, the design decisions underlying this state of affairs have more concrete consequences. First among them: how do you configure a popover that’s being presented by a popover presentation segue? When the presenting view controller receives -prepareForSegue:, the popover presentation controller does not yet exist to be configured.

There are many reasons why the destination view controller cannot step in to configure the popover presentation controller:

  • Storyboards do not inform the destination view controller that they are about to be shown via a segue. (I wish they did. I also wish that Interface Builder would let you change the class of a standard segue to a subclass with an override of -perform.)

  • There might be multiple segues leading to the same destination view controller. Some might be popovers, some might not. The ones that are popovers might all need to be configured differently, which -viewWillAppear doesn’t have enough context to reason about.

  • The destination view controller cannot provide certain information, such as which bar button item to present from or which passthrough views to allow.

  • The destination view controller might need to be replaced entirely when the size class of its presenter’s container changes. This is managed by the popover presentation controller’s delegate. Obviously it would not make sense for the delegate to be the destination view controller, especially if it’s about to go away.

✻ ✻ ✻

More generally, the current API is too oriented at making the presented view controller responsible for aspects of its own presentation. This isn’t new (it’s been like this since modalTransitionStyle was introduced in iOS 3.0), and I understand the idea: reusable components like an About Box, a settings menu, etc. are not truly reusable if every caller has to take steps to configure their presentation the same way.

But UIKit has neglected those cases where the caller does want to control the presentation—where the reusable aspect of a view controller consists only of its content and not its presentation style. As an example, let’s say you’re implementing some sort of VOIP app. You have a view controller which manages an interface for viewing and editing contact details.

This view controller can be shown in two ways. While on a call, tapping a button should flip the view around to show the other party’s contact sheet. Or, just like the iOS Phone app, entering a number at the dialer and hitting the Add Contact button should bring up the same UI in edit mode, as a modal fullscreen presentation.

Clearly the presentation style should be dictated by the call site, not by the presented view controller itself.

iOS 5.0 added providesPresentationContextTransitionStyle for this purpose, but transition controllers and presentation controllers ignore that property completely. (Not that it was a great API anyway; it used the presenter’s modalTransitionStyle—what if the presenter was itself presented, and wanted to control its own presentation transition via this property?)

The current way for a caller to control the presentation of a view controller is to assign an object as the presented view controller’s transitioningDelegate. This works, but it’s weird. It turns what could be a straightforward series of steps at the call site into an inverted jumble of delegates and callbacks:

- (void)presentSomeVC {
  UIViewController *vcToPresent = …;
  vcToPresent.transitioningDelegate = self;
  [self presentViewController:vcToPresent …];
}

- (void)animationControllerForPresentedViewController:… {
  return [MyCustomAnimationController sharedInstance];
}

- (void)interactionControllerForPresentation:… {
  return [MyCustomInteractionController sharedInstance];
}

- (void)presentationControllerForPresentedViewController:… {
  return [MyCustomPresentationController sharedInstance];
}

And who cleans up the transitioningDelegate when it’s done? The API makes an implicit assumption that the presenter will always outlive the presented view controller. Those kinds of assumptions are dangerous and lead to crashes. Since iOS 8 gives us an object that is in charge of the entire presentation process, why can’t we do something like this?

- (void)presentSomeVC {
  UIViewController *vcToPresent = …;

  UIPresentationController *presentation = [UIPresentationController new];
  presentation.presentedViewController = vcToPresent;
  presentation.animationControllerForPresentation = [MyCustomAnimationController sharedInstance];
  presentation.interactionControllerForPresentation = [MyCustomInteractionController sharedInstance];

  [self attachPresentation:presentation];
}

Violà! No dangling delegates, and no compendium of protocols merely define a way to vend objects that conform to yet other protocols.

Of course, view controllers that define their own presentation context should be able to provide presentation controllers themselves, overriding the presentation controller that the presented view controller would normally vend:

- (void)viewDidLoad {
  self.definesPresentationContext = YES;
}

- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)vc
                                                                        source:(UIViewController *)source
{
  // Some descendant tried to present a view controller.
  // We're the closest ancestor that definesPresentationContext.

  // We can either return a UIPresentationController instance to customize the presentation,
  // or we can return nil and the system will ask the presented view controller for one.

  // (If we didn't implement this method, the system would act as if we returned nil.)
}

✻ ✻ ✻

The coolest thing about adopting such an architecture is where we can take it from there. If we have these presentation controller objects, and the presenting view controller can be cleanly responsible for generating a presentation controller, why can’t we reimagine UINavigationController in terms of presentations?

Instead of managing a stack of view controllers, it would actually manage a stack of presentation controllers, each with its own associated stack of view controllers. The default presentation controller returned by the navigation controller would animate with the same layered navigation push animation we all know and love today.

The specific use case I have in mind involves configuration interfaces like those commonly found in the Settings app (for Wi-Fi, iCloud, etc.). In those cases, a table view inside a navigation stack offers a list of configurable options. The configuration interface for each item should offer Cancel and Done buttons, but since the list is already within a navigation controller the configuration interface should not be presented using a modal presentation style.

If UINavigationController could act as the presentation context for its descendants, the configuration interface could be shown via -presentViewController:…, and the UINavigationController could perform the default push animation while adding a new entry onto its existing “presentation stack”, of which the presentedViewController would act as the root. A “Done” button that sent -dismissViewController:… to its view controller would eventually reach the root of the current presentation stack; the navigation controller would be asked to dismiss that stack, and the interface would be returned to the state it was before presentation.

An image depicting a navigation controller managing a stack of presentation controllers, each associated with a stack of view controllers.

✻ ✻ ✻

Radars are forthcoming. Eventually.

One Comment

  1. In the WWDC session, they configure the popoverPresentationController before presenting the popover view controller (and it looks better). The code I’m referring to is on session 228, page 37 of the PDF (I’m using it in an app, and works fine on beta 5). It’s inconsistent with the docs, though.

Comments are closed.