6
votes

I have a UIPopoverController stored in a strong property in my View Controller. When the user rotates the iPad while the popover is visible, I dismiss the popover and set my property to nil.

if (self.popover != nil) {
    [self.popover dismissPopoverAnimated:NO];
    self.popover.delegate = nil;
    self.popover = nil;
}

When the code gets to self.popover = nil, ARC attempts to dealloc the UIPopoverController, but it crashes because it is supposedly still visible.

How am I supposed to dismiss and nil out the popover without it crashing?

3
Nice explanation of the problem. I'm experiencing the exact same situation.funroll

3 Answers

16
votes

First off, it would be advisable to check if the popover is being presented, this will conveniently also check if it is allocated:

if ([self.popover isPopoverVisible]) {
    [self.popover dismissPopoverAnimated:NO];
}

Now, the issue is, you don't get the delegate callback - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController if you dismiss the popover programmatically like this, but you need a strong reference to the popover until it is no longer visible.

The way to do this is delay setting the property to nil until you return to the main run loop, as when you get back to the main run loop, all animations will have finished and thus the popover will no longer be visible.

You will want to move the code setting the popover to nil into another method:

- (void)releasePopover {
    self.popover.delegate = nil;
    self.popover = nil;
}

Then, in your rotation callback, add this method to fire on the main run loop, I like to do this by adding an invocation operation to the main run loop:

if ([self.popover isPopoverVisible]){
    [self.popover dismissPopoverAnimated:NO];
    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(releasePopover) object:nil];
    [[NSOperationQueue mainQueue] addOperation:invocationOperation];
}

Finally, for the sake of cleanliness, you will probably want to call -releasePopover from inside your - (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController callback.

So, putting it all together:

- (void)releasePopover
{
    self.popover.delegate = nil;
    self.popover = nil;
}

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
    if ([self.popover isPopoverVisible]){
        [self.popover dismissPopoverAnimated:NO];
        NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(releasePopover) object:nil];
        [[NSOperationQueue mainQueue] addOperation:invocationOperation];
    }
}

- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
{
    [self releasePopover];
}

Having said all that, unless there is a good reason, you may just want to keep the popover around to reuse and only set it to nil when you get low-memory warnings and/or if your view is unloaded, as Chris Loonam's answer mentioned

1
votes

Try to nil it out in the viewDidUnload, if you really feel it necessary to do so. Since ARC automatically releases objects, I'm not sure if doing this is really necessary.

1
votes

Standing on the shoulders of Simon's answer, here's my fix for the crash:

// set to nil on main queue to prevent "dealloc'd while still visible" exception
dispatch_async(dispatch_get_main_queue(), ^{
        self.popover = nil;
});