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.

UIAdaptivePresentationControllerDelegate

The following viewcontroller will display a popover, and if necessary a close button will be added.

    class PresentingViewController: UIViewController, UIPopoverPresentationControllerDelegate {
        
        // MARK: - UIPopoverPresentationControllerDelegate
        
        func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
            return .fullScreen
        }
        
        func presentationController(_ controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
            guard let navigationController = controller.presentedViewController as? UINavigationController else {
                fatalError("Unexpected type of controller")
            }
            
            let closeButton = UIBarButtonItem(title: "Close", style: .done, target: self, action: #selector(close))
            navigationController.topViewController?.navigationItem.leftBarButtonItem = closeButton
            return navigationController
        }
        
        @objc func close() {
            self.dismiss(animated: true, completion: nil)
        }
        
        // MARK: - View cycle
        
        @objc func popoverAction() {
            let pvc = PopoverViewController()
            let navigationController = UINavigationController(rootViewController: pvc)
            navigationController.modalPresentationStyle = .popover
            navigationController.popoverPresentationController?.delegate = self
            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)
        }
        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 {    
        override func viewDidLoad() {
            self.view.backgroundColor = UIColor.green
        }
    }

My old solution

For historic accuracy, below you can find the manual solution. It's mainly obsolete.

    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().