How can I find the UIPopoverController from the UIViewController being displayed in a popover?

16,269

Solution 1

You would think that this would be simple (the UIViewController even has a private _popoverController property!), but it is not.

The general answer is that you have to save a reference to the UIPopoverController in the UIViewController that it is presenting, at the time the UIViewController is created.

  1. If you are creating the UIPopoverController programmatically, then that's the time to store the reference in your UIViewController subclass.

  2. If you are using Storyboards and Segues, you can get the UIPopoverController out of the segue in the prepareForSegue method:

    UIPopoverController* popover = [(UIStoryboardPopoverSegue*)segue popoverController];
    

Of course, be sure that your segue really is a UIStoryboardPopoverSegue!

Solution 2

My recommendation is to leverage a combination of your own custom property and the private APIs in UIKit. To avoid app store rejection, any private APIs should compile out for release builds, and should be used only to check against your implementation.

First let's build the custom property into a category on UIViewController. This allows some perks in the implementation, and it doesn't require you to go back and derive every class from some custom view controller subclass.

// UIViewController+isPresentedInPopover.h

#import <UIKit/UIKit.h>

@interface UIViewController (isPresentedInPopover)

@property (assign, nonatomic, getter = isPresentedInPopover) BOOL presentedInPopover;

@end

Now for the implementation - we'll be using the Objective C runtime's associated object API to provide the storage for this property. Note that a selector is a nice choice for the unique key used to store the object, as it's automatically uniqued by the compiler and highly unlikely to be used by any other client for this purpose.

// UIViewController+isPresentedInPopover.m

#import "UIViewController+isPresentedInPopover.h"
#import <objc/runtime.h>

@implementation UIViewController (isPresentedInPopover)

- (void)setPresentedInPopover:(BOOL)presentedInPopover
{
    objc_setAssociatedObject(self,
                             @selector(isPresentedInPopover),
                             [NSNumber numberWithBool:presentedInPopover],
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)isPresentedInPopover
{
    NSNumber *wrappedBool = objc_getAssociatedObject(self, @selector(isPresentedInPopover));
    BOOL userValue = [wrappedBool boolValue];
    return userValue ?: [[self parentViewController] isPresentedInPopover];
}

@end

So there's a convenient side effect of using this as a category - you can call up to the parentViewController and see if that is contained in a popover as well. This way you can set the property on, say, a UINavigationController and all of its child view controllers will respond correctly to isPresentedInPopover. To accomplish this with subclasses, you'd be either trying to set this on every new child view controller, or subclassing navigation controllers, or other horrific things.

More Runtime Magic

There is still more that the Objective C Runtime has to offer for this particular problem, and we can use them to jump into Apple's private implementation details and check your own app against it. For release builds, this extra code will compile out, so no need to worry about the all-seeing eye of Sauron Apple when submitting to the store.

You can see from UIViewController.h that there is an ivar defined as UIPopoverController* _popoverController with @package scope. Luckily this is only enforced by the compiler. Nothing is sacred as far as the runtime is concerned, and it's pretty easy to access that ivar from anywhere. We'll add a debug-only runtime check on each access of the property to make sure we're consistent.

// UIViewController+isPresentedInPopover.m

#import "UIViewController+isPresentedInPopover.h"
#import <objc/runtime.h>

@implementation UIViewController (isPresentedInPopover)

- (void)setPresentedInPopover:(BOOL)presentedInPopover
{
    objc_setAssociatedObject(self,
                             @selector(isPresentedInPopover),
                             [NSNumber numberWithBool:presentedInPopover],
                             OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)isPresentedInPopover
{
    NSNumber *wrappedBool = objc_getAssociatedObject(self, @selector(isPresentedInPopover));
    BOOL userValue = [wrappedBool boolValue];

#if DEBUG
    Ivar privatePopoverIvar = class_getInstanceVariable([UIViewController class], "_popoverController");
    UIPopoverController *popover = object_getIvar(self, privatePopoverIvar);
    BOOL privateAPIValue = popover != nil;

    if (userValue != privateAPIValue) {
        [NSException raise:NSInternalInconsistencyException format:
         @"-[%@ %@] "
         "returning %@ "
         "while private UIViewController API suggests %@. "
         "Did you forget to set 'presentedInPopover'?",
         NSStringFromClass([self class]), NSStringFromSelector(_cmd),
         userValue ? @"YES" : @"NO",
         privateAPIValue ? @"YES" : @"NO"];
    }
#endif

    return userValue ?: [[self parentViewController] isPresentedInPopover];
}

@end

When using the property incorrectly, you'll get a message like this on the console:

2012-09-18 14:28:30.375 MyApp[41551:c07] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Consistency error in -[UINavigationController isPresentedInPopover]: returning NO while private UIViewController API suggests YES. Did you forget to set 'presentedInPopover'?'

...but when compiling with the DEBUG flag off or set to 0, it compiles down to the exact same code as before.

For The Free and the Foolhardy

Maybe you're doing Ad-Hoc/Enterprise/personal builds, or you're sufficiently bold to see just what Apple thinks about this one for the App Store. Either way, here's an implementation that just works using the current runtime and UIViewController - no setting properties needed!

// UIViewController+isPresentedInPopover.h

#import <UIKit/UIKit.h>

@interface UIViewController (isPresentedInPopover)

@property (readonly, assign, nonatomic, getter = isPresentedInPopover) BOOL presentedInPopover;

@end

// UIViewController+isPresentedInPopover.m

#import "UIViewController+isPresentedInPopover.h"
#import <objc/runtime.h>

@implementation UIViewController (isPresentedInPopover)

- (BOOL)isPresentedInPopover
{
    Ivar privatePopoverIvar = class_getInstanceVariable([UIViewController class], "_popoverController");
    UIPopoverController *popover = object_getIvar(self, privatePopoverIvar);
    BOOL privateAPIValue = popover != nil;
    return privateAPIValue ?: [[self parentViewController] isPresentedInPopover];
}

@end

Solution 3

Most helpful would probably be to make popover a class variable, so in the .m file of the class that is going to present the popover, do something like this:

    @interface ExampleViewController()
    @property (nonatomic, strong) UIPopoverController *popover
    @end

    @implementation
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    {
        if ([segue.identifier isEqualToString:@"some segue"])
        {
            //prevent stacking popovers
            if ([self.popover isPopoverVisible])
            {
                [self.popover dismissPopoverAnimated:YES];
                self.popover = nil;
            }
            [segue.destinationViewController setDelegate:self];
            self.popover = [(UIStoryboardPopoverSegue *)segue popoverController];
         }
     }
     @end

Solution 4

As @joey wrote above, Apple eliminated the need for the dummy control in iOS 8 with the popoverPresentationController property defined for UIViewController as the "The nearest ancestor in the view controller hierarchy that is a popover presentation controller. (read-only)".

Here is an example in Swift for a UIPopoverPresentationController-based segue defined on a storyboard. In this case, a button has been added programmatically and can be defined in this way as the pop-over's anchor. The sender could also be a selected UITableViewCell or a view from it.

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "showCallout" {
        let button = sender as UIButton
        let calloutViewController = segue.destinationViewController as CalloutViewController
        if let popover = calloutViewController.popoverPresentationController {
            popover.sourceView = button
            popover.sourceRect = button.bounds
        }
    }
}
Share:
16,269
SoftMemes
Author by

SoftMemes

Updated on June 17, 2022

Comments

  • SoftMemes
    SoftMemes about 2 years

    Using an instance of a UIViewController, is there any way I can find the UIPopoverController being used to present it? I would also want to find the UIViewController that displayed the UIPopoverController in the first place.

    I would normally use a delegate or other sort of notification to send a signal from the displayed view controller to the displaying one, but in this case I'm trying to create a reusable custom segue that dismisses the popover and then moves on to another view in the main view.

  • Sterling Archer
    Sterling Archer almost 12 years
    A simpler-looking way to access the private variable is [self valueForKey:@"_popoverController"]. This works because +[UIViewController accessInstanceVariablesDirectly] returns YES. Since the method in my answer depends only on the specification in the header file and not the implementation detail of what accessInstanceVariablesDirectly is going to return, I consider the runtime methods a better choice. If you disagree, feel free to use UIPopoverController *popover = [self valueForKey:@"_popoverController"];
  • Anne
    Anne almost 12 years
    +1 for the nice extensive answer :)
  • n13
    n13 about 11 years
    If you're going to do that why not just set the UIPopoverController in a category variable? Then there's no access to any private APIs or ugly hard coded strings. And you can make a category of UIPopOverController that just sets the variable automatically on present. Anyway I just wanted a better way to dismiss the popover on cancel/done so I ended up using blocks. Callbacks and delegates leaving too much messy code.
  • PapillonUK
    PapillonUK over 10 years
    Wow. Thank you for some sanity! Everyone else - ignore all the reams of complicated theory above and just use this!!!
  • Rudolf Adamkovič
    Rudolf Adamkovič over 10 years
    Without hacking private API's? _UIPopoverView class is private API.
  • roberto.buratti
    roberto.buratti over 10 years
    The method do not use any private API, it simply test for a class name.
  • Rudolf Adamkovič
    Rudolf Adamkovič over 10 years
    The example uses a private symbol (class name) which is part of a private API.
  • Norman H
    Norman H over 10 years
    @RudolfAdamkovic yes, it is a "magic string" but it isn't actually accessing a private API (NSStringFromClass).
  • Rudolf Adamkovič
    Rudolf Adamkovič over 10 years
    @roberto.buratti That's right! You're using private API (symbol name) but you're not calling it directly.
  • AlexeyVMP
    AlexeyVMP over 10 years
    That really works, but I had to call this line only AFTER getting and setting up destination controller.
  • roberto.buratti
    roberto.buratti over 10 years
    Strictly speaking, I am neither using nor calling anything at all... I'm just interrogating a public object about it's class name. I'm sure Apple will never reject this... Perhaps it may stop working tomorrow, but there is no hacking of private API's.
  • Oded Ben Dov
    Oded Ben Dov over 10 years
    Apple is sure to include this functionality in an iOS update. A UIViewController has a presentingViewController for presented modals, navigationContoller for controllers on a navigation stack, so it only follows that a view controller should be aware of who is presenting him inside a popover
  • Jordan H
    Jordan H over 9 years
    @OdedBenDov Wow nice prediction. It has arrived in iOS 8 - popoverPresentationController.
  • Collierton
    Collierton over 9 years
    Thank you, @joey, for the tip. I added a code example of this at the bottom.