2

In the project shown below there is an InitialViewController that has a single button labeled "Show Popover". When that button is tapped the app is supposed to present the second view controller (PopoverViewController) as a popover. The second view controller just has a label saying "Popover!".

storyboard image

This works fine if the InitialViewController takes care of instantiating PopoverViewController, retrieving the popoverPresentationController and then setting the popoverPresentationController's delegate to itself (to InitialViewController). You can see the result, below:

functiong app when delegation occurs in InitialViewController

For maximum reusability, however, it would be great if the InitialViewController did not need to know anything about how the presentation controller is delegated. I think it should be possible for the PopoverViewController to set itself as the popoverPresentationController's delegate. I've tried this in either the viewDidLoad or the viewWillAppear functions of the PopoverViewController. However, the PopoverViewController is presented modally in both cases, as shown below:

enter image description here

All the code is contained in just the InitialViewController and the PopoverViewController. The code used in the failing version of the InitialViewController is shown below:

import UIKit

// MARK: - UIViewController subclass

class InitialViewController: UIViewController {

    struct Lets {
        static let storyboardName = "Main"
        static let popoverStoryboardID = "Popover View Controller"
    }

    @IBAction func showPopoverButton(_ sender: UIButton) {

        // instantiate & present the popover view controller
        let storyboard = UIStoryboard(name: Lets.storyboardName,
                                      bundle: nil )
        let popoverViewController =
            storyboard.instantiateViewController(withIdentifier: Lets.popoverStoryboardID )
        popoverViewController.modalPresentationStyle = .popover
        guard let popoverPresenter = popoverViewController.popoverPresentationController
            else {
                fatalError( "could not retrieve a pointer to the 'popoverPresentationController' property of popoverViewController")
        }
        present(popoverViewController,
                animated: true,
                completion: nil )

        // Retrieve and configure UIPopoverPresentationController
        // after presentation (per
        // https://developer.apple.com/documentation/uikit/uipopoverpresentationcontroller)

        popoverPresenter.permittedArrowDirections = .any
        let button = sender
        popoverPresenter.sourceView = button
        popoverPresenter.sourceRect = button.bounds

    }
}

The code in the failing PopoverViewController is shown below:

import UIKit


// MARK: - main UIViewController subclass

class PopoverViewController: UIViewController {

    // MARK: API
    var factorForMarginsAroundButton: CGFloat = 1.2

    // MARK: outlets and actions
    @IBOutlet weak var popoverLabel: UILabel!

    // MARK: lifecycle

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear( animated )

        // set the preferred size for popover presentations
        let labelSize =
            popoverLabel.systemLayoutSizeFitting( UILayoutFittingCompressedSize )
        let labelWithMargins =
            CGSize(width: labelSize.width * factorForMarginsAroundButton,
                   height: labelSize.height * factorForMarginsAroundButton )
        preferredContentSize = labelWithMargins

        // set the delegate for the popoverPresentationController to self
        popoverPresentationController?.delegate = self

    }
}
// MARK: - UIPopoverPresentationControllerDelegate
//       (inherits from protocol UIAdaptivePresentationControllerDelegate)

extension PopoverViewController: UIPopoverPresentationControllerDelegate
{
    func adaptivePresentationStyle(for controller: UIPresentationController,
                                   traitCollection: UITraitCollection)
        -> UIModalPresentationStyle{
            return .none
    }
}

Is it possible for a view controller that is being presented as a popover to be the delegate for its own popoverPresentationController?

I'm using Xcode 8.0, Swift 3.1 and the target is iOS 10.0

Bill Nattaner
  • 774
  • 7
  • 15

2 Answers2

2

It's certainly possible. You're dealing with a timing issue. You need to set the delegate before viewWillAppear. Unfortunately, there is no convenient view lifecycle function to insert the assignment into, so I did this instead.

In your PopoverViewController class, assign the delegate in an overriden getter. You can make the assignment conditional if you'd like. This creates a permanent relationship, so other code code never "override" the delegate by assigning it.

override var popoverPresentationController: UIPopoverPresentationController? {
    get {
        let ppc = super.popoverPresentationController
        ppc?.delegate = self
        return ppc
    }
}
allenh
  • 6,582
  • 2
  • 24
  • 40
  • Thank you, Allen! I just tried that and it worked perfectly. I agree that it is a timing issue, but I had no clue that overriding a property would provide the correct timing. Can you give us an idea of how you figured that one out? – Bill Nattaner Aug 02 '17 at 19:38
  • Honestly, a little trial and error. I found that the popoverPresentationController didn't exist yet after initialization and viewDidLoad was already to late. So I went the route of inserting myself between whatever part of the system consumes the popoverPresentationController – allenh Aug 02 '17 at 19:52
  • Hmm, I think I get it. ViewDidLoad won't happen unless the popoverPresentationController has been initialized and utilized, so a get of popoverPresentationController must come after the init and before ViewDidLoad. Thank you again for your kind (and very quick) response. – Bill Nattaner Aug 02 '17 at 20:27
0

As @allenh has correctly observed, you need to set the delgate before viewWillAppear, and he has offered a clever solution by setting the delegate by overriding the popoverPresentationController getter.

You could also set the delegate to the popover itself in your showPopover() function between setting modalPresentationStyle and presenting the popover:

let vc = storyboard.instantiateViewController(withIdentifier: Lets.popoverStoryboardID )
vc.modalPresentationStyle = .popover
vc.popoverPresentationController?.delegate = vc
present(vc, animated: true, completion: nil)
JJJSchmidt
  • 820
  • 6
  • 13