2017-11-07 Close button on popover

On iPhone, you usually add a cancel/close button to a modal popover. On iPad, there's usually no need to do so. Users just tap outside the popover to dismiss it. However when you're building an app for the iPad, and you support Split View and Multitasking, you suddenly do need it.

The problem is, how do you detect when to hide the close button, and when to show it. This is currently a bit of a hassle. My approach needs code in both the presenting and the presented viewcontroller:

    class PresentingViewController: UIViewController {    
        private var popoverViewController: PopoverViewController?
        override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
            self.popoverViewController?.doneButtonHidden = (self.traitCollection.horizontalSizeClass == .regular)
        @objc func popoverAction() {
            let pvc = PopoverViewController()
            pvc.doneButtonHidden = (self.traitCollection.horizontalSizeClass == .regular)
            let navigationController = UINavigationController(rootViewController: pvc)
            navigationController.modalPresentationStyle = .popover
            navigationController.popoverPresentationController?.sourceView = self.view
            navigationController.popoverPresentationController?.permittedArrowDirections = .up
            navigationController.popoverPresentationController?.sourceRect = CGRect(x: 40, y: 40, width: 1, height: 0)
            self.present(navigationController, animated: true, completion: nil)
            self.popoverViewController = pvc
        override func viewDidLoad() {
            self.view.backgroundColor = UIColor.yellow
            let barButtonItem = UIBarButtonItem(title: "Popover", style: .plain, target: self, action: #selector(popoverAction))
            self.navigationItem.leftBarButtonItem = barButtonItem
    class PopoverViewController: UIViewController {    
        var doneButtonHidden: Bool = false {
            didSet {
                let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissAction))
                self.navigationItem.leftBarButtonItem = self.doneButtonHidden ? nil : button
        @objc func dismissAction() {
            self.dismiss(animated: true, completion: nil)
        override func viewDidLoad() {
            self.view.backgroundColor = UIColor.green

This works, but requires that you keep a reference to the PresentedViewController, to update the visibility of the close button. I don't much like having a class member variable when it could be a local variable, because it clutters up the code of the PresentingViewController. But this is the most concise and readable code I could come up with.

Note that when you use child ViewControllers, this does not seem to work. The reason is that traitCollectionDidChange() doesn't get called automatically. In that case, it could be acceptable to use viewDidLayoutSubviews().