Peter Steinberger

UIPresentationController - Detecting if We Are Presented as Popover

As we’re finally dropping iOS 7 in PSPDFKit, I’ve lead the project to migrate everything to Apple’s new wonderful UIPresentationController API. Now UIPresentationController is by far not perfect, but it’s a huge step forward coming from UIPopoverController and makes presenting in mixed environments much simpler and cleaner. The beauty: The view controller hierarchy is now correct, where in the older popover controller there were unconnected gaps, and it wasn’t easy to go from the contentViewController to the parent presenter. This required inconvenient redirects when a controller inside a popover wanted to dismiss itself. RIP, UIPopoverController. You won’t be missed.

Detecting if we are in a popover

Sometimes it’s quite useful to know inside the view controller if we are presented as a popover or modally. Of course things like a close button should be managed by the presenter, not the presented object, however there might be other changes (keyboard specifics, moving bar buttons around to have space for the close button, sizing differences). These cannot be determined by the trait classes alone. Initially I was naive and just checked for the existence of .popoverPresentationController - but since the system is adaptive, this object will also be there when we are in a compact trait collection. Turns out, it’s quite hard to correctly detect if we are presented as a popover. Here’s my first attempt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@implementation UIPresentationController (PSPDFAdditions)

// This is more hacky than it should be!
- (UIModalPresentationStyle)pspdf_currentPresentationStyle {
    // We can't use the trait collection from the presentation object, 
    // since that one will always be compact if it's an UIPopoverPresentationController.
    UIViewController *vc = self.presentedViewController.presentingViewController;
    if (vc.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact) {
        UIModalPresentationStyle style = self.adaptivePresentationStyle;
        if (style != UIModalPresentationNone) {
            return style;
        }
    }

    return self.presentationStyle;
}

- (BOOL)pspdf_isPopover {
    return self.pspdf_currentPresentationStyle == UIModalPresentationPopover;
}

@end

@implementation UIViewController (PSPDFPresentationControllerAdditions)

- (UIViewController *)pspdf_outmostParentViewController {
    UIViewController *parent = self;
    while (parent.parentViewController) {
        parent = parent.parentViewController;
    }
    return parent;
}

- (BOOL)pspdf_isPopover {
    return self.pspdf_outmostParentViewController.presentationController.pspdf_isPopover;
}

@end

The evil detail is that UIViewController lazily creates a new _UIFullscreenPresentationController, so simply checking for .presentationController is not always correct, as it doesn’t include the case where we are embedded as a child view controller. Now you might think I’m doing it wrong, but there are some valid cases for this. (We have a container that basically embeds other view controllers completely and just adds a switch control as top bar).

Turns out, Apple has the same problem, since after building all of that I found the (of course!) private _isInPopoverPresentation (click for rage-tweet). Now this is interesting - let’s see how they detect this. (Code approximated as I don’t have access to UIKit’s source code)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (BOOL)pspdf_isInPopoverPresentation {
    return [self pspdf_isInContextOfPresentationControllerOfClass:UIPopoverPresentationController.class];
}

- (BOOL)pspdf_isInContextOfPresentationControllerOfClass:(Class)klass {
    BOOL isInContextOfPresentationController = NO;

    UIViewController *presentingViewController = self.presentingViewController;
    while (presentingViewController) {
        // Apple can just access the ivar, we'll have to use KVC to get the same.
        // This is private API - do not ship this!
        UIPresentationController *presentationController = [self valueForKey:@"_presentationController"];

        isInContextOfPresentationController = YES;
        if ([presentationController isKindOfClass:klass]) {
            break;
        }

        if (presentationController.shouldPresentInFullscreen) {
            isInContextOfPresentationController = NO;
            break;
        }
        presentingViewController = presentingViewController.presentingViewController;
    }

    return isInContextOfPresentationController;
}

The Problem: Apple uses _presentationController or _originalPresentationController for the detection, but we only have presentationController available - which unfortunately here lazily creates a presentation controller when first being accessed. The above is also incorrect - we have a case where we embed a controller full-screen inside another controller, and this code assumes YES, but doesn’t crawl up the parent/child view controller hierarchy, so we end up with a situation where being in a popover is assumed when it’s clearly not:

When we use the above code, everything works:

Notice how we change the background so the popover uses transparency, while modally we use a clear white. The close button is handled externally by the presentation logic. The solution we use is also not 100% correct - cases where we would present inside the context of a popover would likely be incorrectly flagged as modal presentation - but since we don’t use any of that I didn’t investigate further.

Other solutions

Going back, how did we do things before presentation controllers? In PSPDFKit we set a flag on the view controller, however this was prone to error. What Apple and many others are doing is simply looking at the view hierarchy if there is a _UIPopoverView in the tree . This is a used and proven method, and something Apple uses a lot in UIKit as well, and which has quite some usage and similar implementations inside UIKit. ([UIWebDocumentView inPopover], [_UIPopoverView popoverViewContainingView:], [UITextField _inPopover], …)

Looking at the arrow direction

Someone from Twitter pointed out that there’s even another way to detect if the UIPopoverPresentationController is really used to present as popover, as we can look at the arrowDirection - if that is set to UIPopoverArrowDirectionUnknown it’s likely modal and not a popover.

Are there even better ways? Am I missing something here? I’m not happy with any of the above solutions. Ideally I should be able to ask the controller what his or the nearest ancestor’s adapted current modal presentation is. I’ve filed rdar://22048335 for an API extension - maybe we get this in iOS 10.