Making Popovers Adapt to Size Classes

Apple introduced popover presentation controllers that adapt to size classes in iOS 8. So why I am only writing about them now? Well partly because I only just got around to it but mostly as they still seem to cause problems. The popover presentation controller delegate methods are for me some of the most confusing methods in UIKit. So here is my quick and simple introduction to making a popover adapt to size classes.

Presenting a Popover

Let’s get the basic setup out of the way quickly as it is not so interesting. Here is the storyboard for a universal app:

Adaptive Storyboard

The two buttons both trigger a segue of type “Present As Popover”. The first shows a plain view controller, the second shows a table view controller embedded in a navigation controller to allow a further segue to a detail view controller. Here is what the two popovers look like using a regular size class:

Simple popover Embedded popover

We set the popover presentation controller delegate and popover anchor point in the prepareForSegue function of the root view controller:

override func prepareForSegue(segue: UIStoryboardSegue, 
                             sender: AnyObject?) {
  switch segue.identifier {
  case "SimpleSegue"?:
    let simplePPC = segue.destinationViewController.popoverPresentationController
    simplePPC?.delegate = self
    simplePPC?.sourceView = simpleButton
    simplePPC?.sourceRect = simpleButton.bounds

At this point the popovers work and will even by default adapt to full screen modal presentations for compact size classes. Sadly the presentation controller does not add a button to dismiss the popover when we are modal. Let’s fix that first.

The UIPopoverPresentationController Delegate

To add a dismiss button when presenting our popover full screen we need to implement the delegate. Let’s do that in an extension to our view controller class:

extension RootViewController: UIPopoverPresentationControllerDelegate {

Adding the button

Our popover needs a done button when we present it in a modal full screen style. This happens by default on the iPhone and any time our application transitions to a compact style on the iPad (due to rotation or a split screen).

The delegate function we need is presentationController:viewControllerForAdaptivePresentationStyle:. This is where I find the documentation to be confusing. We need to return the view controller for when the popover is shown in a compact width. Since our presented view controller may or may not already be embedded in a navigation controller we handle both cases:

func presentationController(controller: UIPresentationController,
     style: UIModalPresentationStyle) -> UIViewController? {

  if let navController = controller.presentedViewController as? UINavigationController {
    return navController
  } else {
    let navController = UINavigationController.init(rootViewController: controller.presentedViewController)
     return navController

If the presented view controller is a navigation controller (which it is for the second segue) we add the button and return it. Otherwise we embed it as the root of a new navigation controller. The short function to add the dismiss button to the navigation controller in both cases is below:

private func addDismissButton(navigationController: UINavigationController) {
    let rootViewController = navigationController.viewControllers[0]
    rootViewController.navigationItem.leftBarButtonItem = 
        UIBarButtonItem.init(barButtonSystemItem: .Done,
                             target: self, 
                             action: "didDismissPresentedView")

Note that we add the button to the root view controller which is navigationController.viewControllers[0] and not navigationController.topViewController which might be the detail view controller when the transition happens.

We also need the function that dismisses the presented view controller when the user taps the done button:

func didDismissPresentedView() {
  presentedViewController?.dismissViewControllerAnimated(true, completion: nil)

At this point we have a popover that adapts to a full screen modal display when the view transitions to a compact size class. It also has a done button we can use to dismiss it which is good:

regular to compact transition

Removing the button

The final step which often seems to be missed is to remove the button if we transition back to a regular size class. Otherwise you end up with something like this in the navigation bar:

Extra done button

The best place to remove the button that I can find is a method that was only introduced in iOS 8.3. If we are about to present with a non modal style we check for a navigation controller and if present remove the done button:

func presentationController(presentationController: UIPresentationController,
     willPresentWithAdaptiveStyle style: UIModalPresentationStyle,
     transitionCoordinator: UIViewControllerTransitionCoordinator?) {
  if style == .None {
    if let navController = presentationController.presentedViewController
           as? UINavigationController {

The removeDismissButton function is a reverse of the addDismissButton:

private func removeDismissButton(navigationController: UINavigationController) {
  let rootViewController = navigationController.viewControllers[0]
  rootViewController.navigationItem.leftBarButtonItem = nil;

Further Reading

The UIPopoverPresentationControllerDelegate and UIAdaptivePresentationControllerDelegate documentation cover more ways to interact with and change the presentation. The full project code is in my GitHub repository at the link below. You may also want to check out the WWDC 2014 session:

Never Miss A Post

Sign up to get my iOS posts and news direct to your inbox and I'll also send you my free iOS Size Classes Cheat Sheet

    Unsubscribe at any time. See Privacy Policy.

    Archives Categories