iOS 8 presentationController determine if really is popover
Asked Answered
S

8

17

I'm using the new adaptive "Present As Popover" capability of iOS 8. I wired up a simple segue in the StoryBoard to do the presentation. It works great on an iPhone 6 Plus as it presents the view as a popover and on an iPhone 4s it shows as a full screen view (sheet style).

The problem is when shown as a full screen view, I need to add a "Done" button to the view so dismissViewControllerAnimated can be called. And I don't want to show the "done" button when it's shown as a popover.

enter image description here

I tried looking at the properties of both presentationController and popoverPresentationController, and I can find nothing that tells me if it is actually being shown as a popover.

NSLog( @"View loaded %lx", (long)self.presentationController.adaptivePresentationStyle );          // UIModalPresentationFullScreen
NSLog( @"View loaded %lx", (long)self.presentationController.presentationStyle );                  // UIModalPresentationPopover
NSLog( @"View loaded %lx", (long)self.popoverPresentationController.adaptivePresentationStyle );   // UIModalPresentationFullScreen
NSLog( @"View loaded %lx", (long)self.popoverPresentationController.presentationStyle );           // UIModalPresentationPopover

adaptivePresentationStyle always returns UIModalPresentationFullScreen and presentationStyle always returns UIModalPresentationPopover

When looking at the UITraitCollection I did find a trait called "_UITraitNameInteractionModel" which was only set to 1 when it was actually displayed as a Popover. However, Apple doesn't provide direct access to that trait through the traitCollection of popoverPresentationController.

Shove answered 1/11, 2014 at 7:12 Comment(4)
Did you find the solution yet?Uhlan
I think Rob Glassey answer is the most complete. However, Apple should provide a much easier way to do this.Shove
@TodCunningham Apple do provide an easy way, see my answer.Solana
Sorry @Solana but my application is a game, and I didn't want to add a navigation controller just for the purposes of having a done button. Doesn't fit with the games style.Shove
D
13

The best way (least smelly) I've found to do this is to use the UIPopoverPresentationControllerDelegate.

• Ensure the presented view controller is set as the UIPopoverPresentationControllerDelegate on the UIPopoverPresentationController being used to manage the presentation. I'm using a Storyboard so set this in prepareForSegue:

segue.destinationViewController.popoverPresentationController.delegate = presentedVC;

• Create a property in the presented view controller to keep track of this state:

@property (nonatomic, assign) BOOL amDisplayedInAPopover;

• And add the following delegate method (or add to your existing delegate method):

- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
{
    // This method is only called if we are presented in a popover
    self.amDisplayedInAPopover = YES;
}

• And then finally in viewWillAppear: - viewDidLoad: is too early, the delegate prepare method is called between viewDidLoad: and viewWillAppear:

if (self.amDisplayedInAPopover) {
    // Hide the offending buttons in whatever manner you do so
    self.navigationItem.leftBarButtonItem = nil;
}

Edit: Simpler method!

Just set the delegate (making sure your presentedVC adopts the UIPopoverPresentationControllerDelegate):

segue.destinationViewController.popoverPresentationController.delegate = presentedVC;

And supply the method:

- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
{
    // This method is only called if we are presented in a popover
    // Hide the offending buttons in whatever manner you do so
    self.navigationItem.leftBarButtonItem = nil;
}
Darvon answered 22/7, 2015 at 18:37 Comment(7)
How to get the reference of presentedVC inside prepareForSegue?Uhlan
segue.destinationViewController is the place to start looking. Watch out if you have an intervening nav controller before the view controller you think you're presenting, as the destination VC will be the nav controller.Darvon
This is exactly my case. I have a intervening nav controller, so I end up referring my presentedVC as such: segue.destinationViewController.topViewController as? UIPopoverPresentationControllerDelegate and it seems to work.. does it look right?Uhlan
Tried all the other solutions here but this is the only one that works and have least smell. Thanks!Uhlan
Yeah, topViewController actually looks much better than what I've used in the past! (interveningNavigationController.viewControllers.lastObject).Darvon
You could also just directly hide the buttons in the delegate method. No need for tracking state, or modifying viewWillAppear:, unless there's a chance that same instance of the presentedVC will be presented again outside a popover (very unlikely unless you're caching it and reusing it).Darvon
Note that this does not work with adaptive size classes. prepareForPopoverPresentation is invoked every time a full screen becomes a popover, but not the other way round.Depressant
S
10

I check to see if the popoverPresentationController's arrowDirection is set after the view is laid out. For my purposes, this works well enough and covers the case of popovers on smaller screened devices.

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

    if (popoverPresentationController?.arrowDirection != UIPopoverArrowDirection.Unknown) {
        // This view controller is running in a popover
        NSLog("I'm running in a Popover")
    }
}
Sick answered 14/12, 2014 at 14:17 Comment(1)
Have you found a better way since this answer was written?Dobrinsky
A
3

How about

if (self.modalPresentationStyle == UIModalPresentationPopover)

It's working for me

Airboat answered 17/5, 2015 at 22:3 Comment(0)
S
3

The official way to implement this is first remove the Done button from your view controller and second, when adapting to compact embed your view controller in a navigation controller, adding the done button as a navigation item:

func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
    return UIModalPresentationStyle.FullScreen
}

func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
    let navigationController = UINavigationController(rootViewController: controller.presentedViewController)
    let btnDone = UIBarButtonItem(title: "Done", style: .Done, target: self, action: "dismiss")
    navigationController.topViewController.navigationItem.rightBarButtonItem = btnDone
    return navigationController
}

func dismiss() {
    self.dismissViewControllerAnimated(true, completion: nil)
}

Full Tutorial

Screenshots

Solana answered 1/3, 2016 at 13:44 Comment(0)
B
3

I tested all solutions presented in this post. Sorry, none works correctly in all cases. For example in iPad split view presentation style can change while dragging split view line, so we need specific notification for that. After few hours of researches i found solution in apple sample (swift): https://developer.apple.com/library/ios/samplecode/AdaptivePhotos/Introduction/Intro.html#//apple_ref/doc/uid/TP40014636

Here is the same solution in obj-c.

First in prepareForSegue function set the popoverPresentationController delegate. It can be also set in MyViewController "init", but not in "viewDidLoad" (because first willPresentWithAdaptiveStyle is called before viewDidLoad).

MyViewController *controller = [segue destinationViewController];
        controller.popoverPresentationController.delegate = (MyViewController *)controller;

Now MyViewController object will receive this notification every time iOS changes presentation style, including first presenting. Here is example implementation which shows/hides "Close" button in navigationController:

- (void)presentationController:(UIPresentationController *)presentationController
  willPresentWithAdaptiveStyle:(UIModalPresentationStyle)style
         transitionCoordinator:(nullable id<UIViewControllerTransitionCoordinator>)transitionCoordinator {
    if (style == UIModalPresentationNone) {
        // style set in storyboard not changed (popover), hide close button
        self.topViewController.navigationItem.leftBarButtonItem = nil;
    } else {
        // style changed by iOS (to fullscreen or page sheet), show close button
        UIBarButtonItem *closeButton =
            [[UIBarButtonItem alloc] initWithTitle:@"Close" style:UIBarButtonItemStylePlain target:self action:@selector(closeAction)];
        self.topViewController.navigationItem.leftBarButtonItem = closeButton;
    }
}

- (void)closeAction {
    [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
Bespangle answered 12/5, 2016 at 7:54 Comment(0)
S
2

The UIPresentationController which manages your view controller is presenting it by setting the modalPresentationStyle to UIModalPresentationPopover.

As per UIViewController reference:

presentingViewController

  • The view controller that presented this view controller. (read-only)

modalPresentationStyle

  • UIModalPresentationPopover: In a horizontally regular environment, a presentation style where the content is displayed in a popover view. The background content is dimmed and taps outside the popover cause the popover to be dismissed. If you do not want taps to dismiss the popover, you can assign one or more views to the passthroughViews property of the associated UIPopoverPresentationController object, which you can get from the popoverPresentationController property.

We can therefore determine whether your view controller is inside a popover or presented modally by checking the horizontalSizeClass as follows (I assumed your button is a UIBarButtonItem)

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    if (self.presentingViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular)
        self.navigationItem.leftBarButtonItem = nil; // remove the button
}

The safest place to check this is in viewWillAppear: as otherwise the presentingViewController may be nil.

Sarson answered 20/11, 2014 at 11:0 Comment(5)
You can't deduce from the horizontal size class that you are in a popover or not. A view controller on an iPhone has a regular horizontal size class too.Chromatophore
@FredA. check out WWDC 2014 Session 216 @4:24. iPhones (except iPhone 6Plus) have compact size classes both in horizontal and vertical orientation. The posted solution works fine in one of my projects where I'm using the "Present as Popover" adaptive segue to present the popover controller. Isn't is working for you?Sarson
That may work, but it's the same as checking that the userInterfaceIdiom is iPad. Besides, you could force the content to be shown inside a popover even on an iPhone. So I think the question is about checking specifically whether the content is presented in a popover.Chromatophore
Nope, that won't work on iPhone 6Plus in landscape. When using the "Present as Popover" adaptive segue popups will be presented as popups or modally based on the horizontal size class as stated in the quoted docs.Sarson
I understand your point. If you have a popover while in a compact horizontal size, it will be presented as a modal fullscreen. That's the default behavior. Now, check Ray Wenderlich's "iOS 8 by tutorials" chapter 6, you will see that it's now possible (and perfectly legal) to force the use of popovers even on an iPhone in portrait. In that case, how do you know you have to remove the bar button item ? I'm just saying that the solution you propose depends too much on size classes and not on checking we are actually in a popover.Chromatophore
H
2

Solution that works with multitasking

Assign the presenting controller as the popover's delegate

...
controller.popoverPresentationController.delegate = controller;
[self presentViewController:controller animated:YES completion:nil];

Then, in the controller, implement the delegate methods:

- (void)presentationController:(UIPresentationController *)presentationController willPresentWithAdaptiveStyle:(UIModalPresentationStyle)style transitionCoordinator:(id<UIViewControllerTransitionCoordinator>)transitionCoordinator
{
    if (style != UIModalPresentationNone)
    {
        // Exited popover mode
        self.navigationItem.leftBarButtonItem = button;
    }
}

- (void)prepareForPopoverPresentation:(UIPopoverPresentationController *)popoverPresentationController
{
    // Entered popover mode
    self.navigationItem.leftBarButtonItem = nil;
}
Hawes answered 6/10, 2016 at 23:33 Comment(0)
M
1

My tricky solution, works perfectly.

In the PopoverViewController's viewDidLoad.

if (self.view.superview!.bounds != UIScreen.main.bounds) {
    print("This is a popover!")
}

The idea is simple, A Popover's view size is never equal to the device screen size unless it's not a Popover.

Monasticism answered 30/11, 2017 at 2:58 Comment(2)
This worked perfectly for me, but had to do the check in viewDidAppear.Lancastrian
In my case I needed to check the view's bounds instead of the superview's bounds, and check it in viewWillAppear or later. Objective-C: bool isFullScreen = CGRectEqualToRect(self.view.bounds, [UIScreen mainScreen].bounds);Burtis

© 2022 - 2024 — McMap. All rights reserved.