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.

Last updated: Jun 12, 2020

Presenting a Popover

Let’s get the basic setup out of the way quickly as it’s not so interesting. Here’s the storyboard for our App:

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’s how the two popovers look when running fullscreen on an iPad:

Simple popover Embedded popover

I set the popover presentation controller delegate and popover anchor point in the prepare(for:sender:) function of the root view controller:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  switch segue.identifier {
  case "SimpleSegue"?:
    let simplePPC = segue.destination.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. In iOS 13, the card style modal presentation can be dismissed with a drag gesture. If we’re supporting iOS 12 or earlier we’ll need to 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. I’ll add the button to the navigation bar of a navigation controller. If our presented view controller is not already embedded in a navigation controller I’ll create one.

The delegate function we need is presentationController:viewControllerForAdaptivePresentationStyle: which is inherited from the UIAdaptivePresentationControllerDelegate protocol. 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:

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

We first check if we have a modal presentation style. If not, we just return the current presented view controller unchanged:

  if style == .none {
    return controller.presentedViewController
  }

I then check if the presented view controller is a navigation controller. It it’s not I create one to wrap the presented view controller:

  // Find or create the navigation controller
  let navigationController: UINavigationController = {
    guard let navigationController = controller.presentedViewController 
      as? UINavigationController else {
      return UINavigationController(rootViewController: controller.presentedViewController)
    }
    return navigationController
  }()

Once we have a navigation controller we can add the dismiss button to it and return it:

  addDismissButton(navigationController)
  return navigationController
}

I have a short helper function to add the dismiss button to the navigation controller:

private func addDismissButton(_ navigationController: UINavigationController) {
  let rootViewController = navigationController.viewControllers[0]
  rootViewController.navigationItem.leftBarButtonItem = 
    UIBarButtonItem.init(barButtonSystemItem: .Done,
    target: self, action: #selector(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:

@objc private func didDismissPresentedView() {
  presentedViewController?.dismiss(animated: 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:

Popover in modal style with done button

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,
     let navController = presentationController.presentedViewController as? UINavigationController {
     removeDismissButton(navController)
   }
}

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: