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.

Tod Cunningham
  • 3,691
  • 4
  • 30
  • 32

8 Answers8

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;
}
Rob Glassey
  • 2,237
  • 20
  • 21
  • How to get the reference of presentedVC inside prepareForSegue? – Bruce Aug 02 '15 at 07:22
  • 1
    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. – Rob Glassey Aug 02 '15 at 07:26
  • 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? – Bruce Aug 02 '15 at 07:52
  • Tried all the other solutions here but this is the only one that works and have least smell. Thanks! – Bruce Aug 02 '15 at 08:20
  • Yeah, topViewController actually looks much better than what I've used in the past! (interveningNavigationController.viewControllers.lastObject). – Rob Glassey Aug 02 '15 at 09:05
  • 1
    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). – Rob Glassey Aug 02 '15 at 09:32
  • 1
    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. – Frederik Winkelsdorf Mar 27 '16 at 10:43
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")
    }
}
John E. Ray
  • 636
  • 6
  • 4
3

How about

if (self.modalPresentationStyle == UIModalPresentationPopover)

It's working for me

Scroot
  • 41
  • 1
  • 1
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

malhal
  • 26,330
  • 7
  • 115
  • 133
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];
}
Slyv
  • 421
  • 3
  • 8
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.

Tomas Camin
  • 9,996
  • 2
  • 43
  • 62
  • 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. – Frederic Adda Nov 24 '14 at 20:44
  • @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? – Tomas Camin Nov 25 '14 at 08:56
  • 1
    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. – Frederic Adda Nov 25 '14 at 09:51
  • 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. – Tomas Camin Nov 25 '14 at 09:58
  • 2
    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. – Frederic Adda Nov 25 '14 at 10:14
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;
}
beebcon
  • 6,893
  • 5
  • 25
  • 27
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.

Alice Chan
  • 2,814
  • 2
  • 16
  • 16
  • This worked perfectly for me, but had to do the check in viewDidAppear. – Chuck Boris Dec 14 '17 at 10:54
  • 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);` – jk7 Aug 17 '18 at 02:51