73

I've created a popover from a UIBarButtonItem using Xcode Storyboards (so there's no code) like this:

Xcode 5.0 Connections Inspector with Popover

Presenting the popover works just fine. However, I can't get the popover to disappear when I tap the UIBarButtonItem that made it appear.

When the button is pressed (first time) the popover appears. When the button is pressed again (second time) the same popover appears on top of it, so now I have two popovers (or more if I continuer pressing the button). According to the iOS Human Interface Guidelines I need to make the popover appear on the first tap and disappear on the second:

Ensure that only one popover is visible onscreen at a time. You should not display more than one popover (or custom view designed to look and behave like a popover) at the same time. In particular, you should avoid displaying a cascade or hierarchy of popovers simultaneously, in which one popover emerges from another.

How can I dismiss the popover when the user taps the UIBarButtonItem for a second time?

Sam Spencer
  • 8,492
  • 12
  • 76
  • 133
  • How did you create the segue? Is the source end of the segue the button or the view controller? Did you set any passthroughs for the segue? – rob mayoff Nov 27 '11 at 22:03
  • @rob I created the Segue through Interface builder. I would select the button and drag the Popover Segue to the Master View that I wanted. The image above shows this. I am not sure what you mean by the last two questions in your comment. – Sam Spencer Nov 27 '11 at 22:17
  • 2
    I created a new project using the "Single View Application" template and storyboard. I dragged a button to the template's view and dragged out a second view controller. I control-dragged from the button to the second VC and chose Popover. When I run this, I can touch the button to make the popover appear and then when I touch anywhere outside the popover (including on the button), the popover disappears. What did you do differently? – rob mayoff Nov 27 '11 at 23:46
  • I agree with the above comment. Tapping on the button a second time dismisses the popover. – memmons Mar 26 '12 at 18:09
  • 2
    @robmayoff you are right that it works fine with UIButton, but try the same steps with a UIBarButton item in a toolbar. I didn't see that at first either, so I edited RazorSharp's question to make it more clear. – Matt Andersen Jul 27 '12 at 18:02

6 Answers6

114

EDIT: These problems appear to be fixed as of iOS 7.1 / Xcode 5.1.1. (Possibly earlier, as I haven't been able to test all versions. Definitely after iOS 7.0, since I tested that one.) When you create a popover segue from a UIBarButtonItem, the segue makes sure that tapping the popover again hides the popover rather than showing a duplicate. It works right for the new UIPresentationController-based popover segues that Xcode 6 creates for iOS 8, too.

Since my solution may be of historical interest to those still supporting earlier iOS versions, I've left it below.


If you store a reference to the segue's popover controller, dismissing it before setting it to a new value on repeat invocations of prepareForSegue:sender:, all you avoid is the problem of getting multiple stacking popovers on repeated presses of the button -- you still can't use the button to dismiss the popover as the HIG recommends (and as seen in Apple's apps, etc.)

You can take advantage of ARC zeroing weak references for a simple solution, though:

1: Segue from the button

As of iOS 5, you couldn't make this work with a segue from a UIBarButtonItem, but you can on iOS 6 and later. (On iOS 5, you'd have to segue from the view controller itself, then have the button's action call performSegueWithIdentifier: after checking for the popover.)

2: Use a reference to the popover in -shouldPerformSegue...

@interface ViewController
@property (weak) UIPopoverController *myPopover;
@end

@implementation ViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // if you have multiple segues, check segue.identifier
    self.myPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
}
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
    if (self.myPopover) {
        [self.myPopover dismissPopoverAnimated:YES];
        return NO;
    } else {
        return YES;
    }
}
@end

3: There's no step three!

The nice thing about using a zeroing weak reference here is that once the popover controller is dismissed -- whether programmatically in shouldPerformSegueWithIdentifier:, or automatically by the user tapping somewhere else outside the popover -- the ivar goes to nil again, so we're back to our initial state.

Without zeroing weak references, we'd have to also:

  • set myPopover = nil when dismissing it in shouldPerformSegueWithIdentifier:, and
  • set ourself as the popover controller's delegate in order to catch popoverControllerDidDismissPopover: and also set myPopover = nil there (so we catch when the popover is automatically dismissed).
rickster
  • 124,678
  • 26
  • 272
  • 326
  • 3
    Props to @wcochran for help figuring this out. – rickster Apr 20 '12 at 05:21
  • +1 great answer, thank you! Love how the ivar is declared in the @implementation – Dan Fairaizl May 01 '12 at 04:04
  • 1
    Thanks. Encapsulation is good! (In fact, Apple's latest version of the primary [ObjC language documentation](http://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocDefiningClasses.html) treats implementation ivars as the default.) – rickster May 25 '12 at 05:27
  • I spent a whole day trying all sorts of stuff like myPopOver = [segue destinationViewController] etc and all that was not working. I just needed a way to reference the popoverViewController so I can dismiss it after a timeout. Couldn't find an answer for it too. But this,, is amazing. Soo short and straight forward. THANK YOU – pnizzle Jun 28 '12 at 05:58
  • Thanks ! this is the best way i've seen so far !! But I have a little issue : if an IBAction and a Segue are associated to a button (UIToolbarButtonItem in my case) then only the segue is performed... EDIT : Part 1 of your answer answers this too. perfect ! – dvkch Jul 26 '12 at 14:33
  • The zeroing weak reference ivar is unnecessary in post-ARC code, a (more conventional) weak property (`@property (weak, nonatomic) UIPopoverController *myPopover;`) will behave identically here. See [Apple's ARC documentation on weak properties](http://developer.apple.com/library/mac/#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html): "if the MyClass instance is deallocated, the property value is set to nil instead of remaining as a dangling pointer." Or am I missing something? (And thank you for the fantastic answer!) – robenkleene Sep 14 '12 at 16:27
  • 2
    Yes, a `__weak` ivar and a `weak` property are equivalent here. Whether to use a property or ivar for something internal to your class remains a subject of much debate. I'm inclined to stick with an ivar when I suspect custom accessors and KVO will never be needed, but the "properties for everything" strategy has its merits, too. – rickster Sep 14 '12 at 19:07
  • 1
    In the iOS6 version, how does a second tap dismiss the popover? It doesn't, does it? – mahboudz Oct 24 '12 at 11:30
  • 1
    The solution is missing the bit where the popover is actually dismissed: [myPopover dismissPopoverAnimated:YES] – scot Mar 30 '13 at 09:27
  • Simplified my answer now that the problem seems to be fixed. (Some of the comments here might not make sense unless you refer to the edit history.) – rickster Jun 10 '14 at 23:56
  • 1
    `[UIStoryboardPopoverPresentationSegue popoverController]: unrecognized selector sent to instance ` But failed at `self.myPopover = [(UIStoryboardPopoverSegue *)segue popoverController];` – Gank Jan 13 '15 at 06:20
13

I found the solution here https://stackoverflow.com/a/7938513/665396 In first prepareForSegue:sender: store in a ivar/property the pointer to the UIPopoverController and user that pointer to dismiss the popover in the subsequent invocations.

...
@property (nonatomic, weak) UIPopoverController* storePopover;
...

- (void)prepareForSegue:(UIStoryboardSegue *)segue 
                 sender:(id)sender {
if ([segue.identifier isEqualToString:@"My segue"]) {
// setup segue here

[self.storePopover dismissPopoverAnimated:YES];
self.storePopover = ((UIStoryboardPopoverSegue*)segue).popoverController;
...
}
Community
  • 1
  • 1
jorgecarreira
  • 301
  • 2
  • 6
  • 1
    Thanks for this. What I did is store the `popoverController` in the `destinationViewController` so I could easily access it later when my custom delegate would callback. – Besi Jan 21 '12 at 21:48
2

I solved it creating a custom ixPopoverBarButtonItem that either triggers the segue or dismisses the popover being shown.

What I do: I toggle the action & target of the button, so it either triggers the segue, or disposes the currently showing popover.

It took me a lot of googling for this solution, I don't want to take the credits for the idea of toggling the action. Putting the code into a custom button was my approach to keep the boilerplate code in my view to a minimum.

In the storyboard, I define the class of the BarButtonItem to my custom class:

custom bar button

Then I pass the popover created by the segue to my custom button implementation in the prepareForSegue:sender: method:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender  
{
    if ([segue.identifier isEqualToString:@"myPopoverSegue"]) {
        UIStoryboardPopoverSegue* popSegue = (UIStoryboardPopoverSegue*)segue;
        [(ixPopoverBarButtonItem *)sender showingPopover:popSegue.popoverController];
    }
}

Btw... since I have more than one buttons triggering popovers, I still have to keep a reference of the currently displayed popover and dismiss it when I make the new one visible, but this was not your question...

Here is how I implemented my custom UIBarButtonItem:

...interface:

@interface ixPopoverBarButtonItem : UIBarButtonItem

- (void) showingPopover:  (UIPopoverController *)popoverController;

@end

... and impl:

#import "ixPopoverBarButtonItem.h"
@interface ixPopoverBarButtonItem  ()
@property (strong, nonatomic) UIPopoverController *popoverController;
@property (nonatomic)         SEL                  tempAction;           
@property (nonatomic,assign)  id                   tempTarget; 

- (void) dismissPopover;

@end

@implementation ixPopoverBarButtonItem

@synthesize popoverController = _popoverController;
@synthesize tempAction = _tempAction;
@synthesize tempTarget = _tempTarget;

-(void)showingPopover:(UIPopoverController *)popoverController {

    self.popoverController = popoverController;
    self.tempAction = self.action;
    self.tempTarget = self.target;
    self.action = @selector(dismissPopover);
    self.target = self;
}    

-(void)dismissPopover {
    [self.popoverController dismissPopoverAnimated:YES];
    self.action = self.tempAction;
    self.target = self.tempTarget;

    self.popoverController = nil;
    self.tempAction = nil;
    self.tempTarget = nil;
}


@end

ps: I am new to ARC, so I am not entirely sure if I am leaking here. Please tell me if I am...

  • 1
    Awesome! That is a great approach, and works well. ARC does most memory management for you, so you never need to use release, retain, etc. This is a godd article about ARC: http://longweekendmobile.com/2011/09/07/objc-automatic-reference-counting-in-xcode-explained/ – Sam Spencer Apr 05 '12 at 21:49
2

I have solved this problem with no need to keep a copy of a UIPopoverController. Simply handle everything in storyboard (Toolbar, BarButtons. etc.), and

  • handle visibility of the popover by a boolean,
  • make sure there is a delegate, and it is set to self

Here is all the code:

ViewController.h

@interface ViewController : UIViewController <UIPopoverControllerDelegate>
@end

ViewController.m

@interface ViewController ()
@property BOOL isPopoverVisible;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.isPopoverVisible = NO;
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // add validations here... 
    self.isPopoverVisible = YES;
    [[(UIStoryboardPopoverSegue*)segue popoverController] setDelegate:self];
}

- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
    return !self.isPopoverVisible;
}

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
    self.isPopoverVisible = NO;
}
@end
Q8i
  • 1,767
  • 17
  • 25
2

I've used custom segue for this.

1

create custom segue to use in Storyboard:

@implementation CustomPopoverSegue
-(void)perform
{
    // "onwer" of popover - it needs to use "strong" reference to retain UIPopoverReference
    ToolbarSearchViewController *source = self.sourceViewController;
    UIViewController *destination = self.destinationViewController;
    // create UIPopoverController
    UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:destination];
    // source is delegate and owner of popover
    popoverController.delegate = source;
    popoverController.passthroughViews = [NSArray arrayWithObject:source.searchBar];
    source.recentSearchesPopoverController = popoverController;
    // present popover
    [popoverController presentPopoverFromRect:source.searchBar.bounds 
                                       inView:source.searchBar
                     permittedArrowDirections:UIPopoverArrowDirectionAny
                                     animated:YES];

}
@end

2

in view controller that is source/input of segue e.g. start segue with action:

-(void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar
{
    if(nil == self.recentSearchesPopoverController)
    {
        NSString *identifier = NSStringFromClass([CustomPopoverSegue class]);
        [self performSegueWithIdentifier:identifier sender:self];
    } 
}

3

references are assigned by segue which creates UIPopoverController - when dismissing popover

-(void)searchBarTextDidEndEditing:(UISearchBar *)searchBar
{
    if(self.recentSearchesPopoverController)
    {
        [self.recentSearchesPopoverController dismissPopoverAnimated:YES];
        self.recentSearchesPopoverController = nil;
    }    
}

regards, Peter

1

I took rickster's answer and packaged it into a class derived from UIViewController. This solution does require the following:

  • iOS 6 (or later) with ARC
  • Derive your view controller from this class
  • make sure to call the "super" versions of prepareForSegue:sender and shouldPerformSegueWithIdentifier:sender if you are overriding those methods
  • Use a named popover segue

The nice thing about this is you don't have to do any "special" coding to support the proper handling of Popovers.

Interface:

@interface FLStoryboardViewController : UIViewController
{
    __strong NSString            *m_segueIdentifier;
    __weak   UIPopoverController *m_popoverController;
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender;
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender;
@end

Implementation:

@implementation FLStoryboardViewController

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if( [segue isKindOfClass:[UIStoryboardPopoverSegue class]] )
    {
        UIStoryboardPopoverSegue *popoverSegue = (id)segue;

        if( m_popoverController  ==  nil )
        {
            assert( popoverSegue.identifier.length >  0 );    // The Popover segue should be named for this to work fully
            m_segueIdentifier   = popoverSegue.identifier;
            m_popoverController = popoverSegue.popoverController;
        }
        else
        {
            [m_popoverController dismissPopoverAnimated:YES];
            m_segueIdentifier = nil;
            m_popoverController = nil;
        }
    }
    else
    {
        [super prepareForSegue:segue sender:sender];
    }
}


- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
    // If this is an unnamed segue go ahead and allow it
    if( identifier.length != 0 )
    {
        if( [identifier compare:m_segueIdentifier]  ==  NSOrderedSame )
        {
            if( m_popoverController == NULL )
            {
                m_segueIdentifier = nil;
                return YES;
            }
            else
            {
                [m_popoverController dismissPopoverAnimated:YES];
                m_segueIdentifier = nil;
                m_popoverController = nil;
                return NO;
            }
        }
    }

    return [super shouldPerformSegueWithIdentifier:identifier sender:sender];
}

@end

Source available on GitHub

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