15

I'm using a UIStoryboardPopoverSegue to present a popover for an iOS 5 iPad app. The Segue works great, but it seems like the toolbar that contains the button is a passthrough view for the popover controller so if you keep pressing the button, more popovers appear. As I'm not creating and keeping track of the UIPopoverController myself (as the Storyboard is doing it) I can't dismiss it when the button is touched again. Has anyone else run into this? I have a bug open with Apple but they haven't responded.

EDIT: I've solved this using the answer below. Here is the code I ended up using. currentPopover is a __weak ivar in my view controller class, so when the controller is done it will drop to nil automatically.

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
    if([segue isKindOfClass:[UIStoryboardPopoverSegue class]]){
        // Dismiss current popover, set new popover
        [currentPopover dismissPopoverAnimated:YES];
        currentPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
    }
}
Cory Imdieke
  • 14,140
  • 8
  • 36
  • 46

7 Answers7

7

There are some visual issues with the your solution Cory.

Two options that can be considered - simply remove or change the action of the button that presents the popover.

Option 1, hold a pointer to the button's action, and after the popover is presented, set the action to nil. Upon dismissal of the popover reset to the original action.

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{

    action = [sender action];
    [sender setAction:nil];

    self.currentPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
    self.currentPopover.delegate = self;
}

-(BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
    [self.navigationItem.rightBarButtonItem setAction:action];

    return YES;
}

This way the popover can only appear once, and will be dismissed as expected.

A second option would be to change the function of the button so that when the popover is visible, tapping the button will cause the popover to be dismissed.

    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    {

        action = [sender action];
        target = [sender target];

        [sender setTarget:self];
        [sender setAction:@selector(dismiss:)];

        self.currentPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
        self.currentPopover.delegate = self;
    }

-(void)dismiss:(id)sender
{
    [self.navigationItem.rightBarButtonItem setAction:action];
    [self.navigationItem.rightBarButtonItem setTarget:target];
    ////or
//  [sender setAction:action];
//  [sender setTarget:target];
    [self.currentPopover dismissPopoverAnimated:YES];
}


    -(BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
    {
        [self.navigationItem.rightBarButtonItem setAction:action];
        [self.navigationItem.rightBarButtonItem setTarget:target];

        return YES;
    }
  • Robert's solution works perfectly fine for me. I think this is how apple implements this technique in the UISplitViewController. Just an add-on, you need to add two ivars: SEL action; id targer; Cheers! – Raunak Dec 27 '11 at 10:36
  • Excellent! The only thing I would add to this is another ivar: id myPopButton, and set this in prepareForSegue: myPopButton = sender; This way, you can reference multiple buttons (when called from a toolbar, 'frinstance). In 'dismiss' and 'popoverControllerShouldDismissPopover' methods, call: [myPopButton setAction: action]; and [myPopButton setTarget: target]; Tested this and it works great (iOS SDK 6.0). – mpemburn Jan 11 '13 at 12:11
3

Simply connect a UIBarButtonItem via IBAction. Use the itendifier set in interface builder:

-(IBAction)barButtonItemPressed:(id)sender {
    if (currentPopoverController && currentPopoverController.popoverVisible) {
        [currentPopoverController dismissPopoverAnimated:YES];
        currentPopoverController = nil;
    } else {
        [self performSegueWithIdentifier:@"aSegueIdentifier" sender:sender];
    }
}

Get a reference of the new UIPopoverCOntroller from the seque:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([[segue identifier] isEqualToString:@"aSegueIdentifier"])
        currentPopoverController = [(UIStoryboardPopoverSegue *)segue popoverController];
}

currentPopoverController is an instance variable, defined in header file:

UIPopoverController *currentPopoverController;

Important: The anchor property of the seque must be set to the corresponding UIBarButtonItem!

Der Ditsch
  • 66
  • 4
  • Unless you also set `currentPopoverController`'s delegate nil out your reference to it on `didDismiss`, you'll still get the multiple popovers problem here. [My preferred workaround](http://stackoverflow.com/a/10238581/957768) is mostly the same as this, but I make `currentPopoverController` a weak reference -- that way it automatically gets `nil`'d out regardless of how the popover is dismissed. – rickster Apr 20 '12 at 07:30
2

I prefer to use a static weak variable, which keeps everything together in one place:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender  
{
    if ([[segue identifier] isEqualToString:@"showSomething"]) {
        static __weak UIPopoverController* currentPopover = nil;
        [currentPopover dismissPopoverAnimated:NO];
        currentPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
        // ...
    }
}

There's no reason to add a separate extra variable (when are you going to have multiple instances of the view controller?), and this way you can add an extra variable for each if() block.

Andres Kievsky
  • 3,461
  • 32
  • 25
2

You have to store a reference to the popoverController property passed as part of the UIStoryboardPopoverSegue class in the prepareForSegue class method.

To access it, over-ride the method in the calling view controller like this:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    // The Storyboard Segue is named popover in this case:
    if ([segue.identifier compare:@"popover"] == NSOrderedSame) {
        // segue.popoverController is only present in popover segue's
        // self.seguePopoverController is a UIPopoverController * property.
        self.seguePopoverController = segue.popoverController;
    }
}

Then you can dismiss it in the usual way.

lnafziger
  • 25,760
  • 8
  • 60
  • 101
  • While I was hoping for something a bit more automated from Apple, this does work. I've added the code I used in my original post for anyone interested. – Cory Imdieke Oct 18 '11 at 00:23
  • Amazing! This approach crashes on code compiled for iOS 7 running on iPads with iOS 8.2. You have to use unwind segues to dismiss the popovers. Thanks apple. – Duck Apr 07 '15 at 21:45
2

This solution could also have visual issues, but it doesn't for my simple case. In my case, the popover was just displaying some help. I put together the following (with ARC) that will dismiss the popover viewcontrollers when the button bar button is pressed a second time (both the original and the newly created one).

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

        if( m_popoverController.popoverVisible )
        {
            [m_popoverController dismissPopoverAnimated:NO];
            dispatch_async( dispatch_get_main_queue(), ^{
                [popoverController dismissPopoverAnimated:YES];
            });
            m_popoverController = nil;
        }
        else
            m_popoverController = popoverController;        
    }    
}

I also added some cleanup in dealloc

- (void)dealloc
{
    if( m_popoverController.popoverVisible )
        [m_popoverController dismissPopoverAnimated:YES];
}

It does require a member variable in your class

UIPopoverController *m_popoverController;
Tod Cunningham
  • 3,691
  • 4
  • 30
  • 32
1

June 14 2013

Thanks for edit in question. Rather than dismissing and recreating the view controller - to avoid performance and battery concerns and prevent Flash when dismissing and recreating view controller - how about preventing the second instance of popover from popping?

//place in view controller (tested iOS6+, iPad, iPhone)
__weak UIPopoverController *popover;
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if([segue isKindOfClass:[UIStoryboardPopoverSegue class]] 
        && [segue.identifier isEqualToString:@"mySegue"]) //remember to change "mySegue" 
            popover = [(UIStoryboardPopoverSegue *)segue popoverController];
}
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
    if ([identifier isEqualToString:@"mySegue"]) //remember to change "mySegue"
        return !popover;
    else
        return YES;
}
added checks to: http://stackoverflow.com/a/10238581/1705353 
Dickey Singh
  • 713
  • 1
  • 8
  • 16
  • 1
    I like it. Could combine the code from my original post's edit with your shouldPerform code and that would make it a bit more visually pleasing than my edit code by itself. – Cory Imdieke Jun 25 '13 at 18:35
0

This is also good.

@interface ViewController : UIViewController <UIPopoverControllerDelegate> {
    UIPopoverController * seguePopoverController;
}

@property (strong) UIPopoverController * seguePopoverController;

@end

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender  
{
    if (self.seguePopoverController) {
        [self.seguePopoverController dismissPopoverAnimated:NO];
        self.seguePopoverController = nil;
    }

    // The Storyboard Segue is named popover in this case:
    if ([[segue identifier] isEqualToString:@"popover"]) {

        UIStoryboardPopoverSegue* popSegue = (UIStoryboardPopoverSegue*)segue;
        UIPopoverController *thePopoverController = [popSegue popoverController];
        thePopoverController.delegate = self;
        self.seguePopoverController = thePopoverController;

    }
}

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
    self.seguePopoverController = nil;
}
Michal Zaborowski
  • 5,039
  • 36
  • 35
  • If you use the code I added to the question, the weak ivar will drop to nil automatically which will erase the need for a bunch of that code. The code up top will also work if you add another popover segue and forget to name it "popover". – Cory Imdieke Nov 08 '11 at 21:55