Search code examples
iosobjective-ccocoa-touchuinavigationbaruipopovercontroller

Crash when calling setRightBarButtonItems:animated: multiple times, why?


I have two methods that toggle the right bar button items in a view controller's UINavigationItem. It's basically to show a search bar in the navigation bar when the search button is tapped, and to hide the search bar when the user executes a search

- (void)displaySearchUI
{
    [self.mainNavigationController.topViewController.navigationItem setRightBarButtonItems:[self searchOpenItems] animated:YES];
    [self.searchBar becomeFirstResponder];
}

- (void)dismissSearchUI
{
    [self.mainNavigationController.topViewController.navigationItem setRightBarButtonItems:[self searchClosedItems] animated:YES];
}

- (NSArray *)searchClosedItems
{
    return @[[self someCustomItem],
             [self someOtherItem],
             [self searchButtonItem],
             ];
}

- (NSArray *)searchOpenItems
{
    return @[[self someCustomItem],
             [self someOtherItem],
             [self searchBarItem],
             ];
}

the item returned from searchButtonItem uses a UIBarButtonItem with an image, while the item returned from searchBarItem has a custom view of a UISearchBar. the other items in each are identical in each search state.

When I call displaySearchUI, the searchbar becoming the first responder opens a popover to show some search suggestions. When the popover is dismissed by the user, dismissSearchUI is called via the popoverControllerDidDismissPopover: delegate method. However at that point, often the app encounters a crash within UINavigationBar:

-[CALayer enumerateObjectsUsingBlock:]: unrecognized selector sent to instance 0x7de9ac80
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[CALayer enumerateObjectsUsingBlock:]: unrecognized selector sent to instance 0x7de9ac80'
*** First throw call stack:
(
    0   CoreFoundation                      0x056f0946 __exceptionPreprocess + 182
    1   libobjc.A.dylib                     0x05379a97 objc_exception_throw + 44
    2   CoreFoundation                      0x056f85c5 -[NSObject(NSObject) doesNotRecognizeSelector:] + 277
    3   CoreFoundation                      0x056413e7 ___forwarding___ + 1047
    4   CoreFoundation                      0x05640fae _CF_forwarding_prep_0 + 14
    5   UIKit                               0x03ba38aa -[UINavigationBar _setLeftViews:rightViews:] + 3774
    6   UIKit                               0x03b8f77e -[UINavigationItem updateNavigationBarButtonsAnimated:] + 186
    7   UIKit                               0x03b901a0 -[UINavigationItem setObject:forLeftRightKeyPath:animated:] + 600
    8   UIKit                               0x03b9100a -[UINavigationItem setRightBarButtonItems:animated:] + 104
    9   MyApp                               0x0028088b -[DisplayManager dismissSearchUI] + 283

I'm not sure exactly what could be happening here. It seems to happen whether or not setting the items is animated. It also only happens when the user taps outside the popover. If I programmatically dismiss the popover and call dismissSearchUI, there's no crash (I imagine due to the different order in which the animations occur, or due to some state differences in the UI when the user dismisses a popover vs a programmatic dismissal. I've tried to work around it by doing this:

#pragma mark - UIPopoverControllerDelegate

- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
    [self dismissSearchSuggestionsUI];
    return NO;
}

- (void)dismissSearchSuggestionsUI
{
    [self.popoverController dismissPopoverAnimated:YES];
    [self.displayManager dismissSearchUI];
}

But I get an EXC_BAD_ACCESS crash instead here, (with a similar stack trace that leads me to believe it's the same issue, just a different symptom due to timing).

Has anyone encountered something like this, and is there a way to work around this without having to disable manual user popover dismissals altogether?


Solution

  • Bah, i figured it out right after posting. By ammending dismissSearchUI's method to do this:

    - (void)dismissSearchUI
    {
        [self.searchBar resignFirstResponder]
        [self.mainNavigationController.topViewController.navigationItem setRightBarButtonItems:[self searchClosedItems] animated:YES];
    }
    

    I resolved the issue. I figured that the search bar would resign its own first responder when the user executed a search, but that doesn't happen automatically when the user manually cancels the search which causes the bar button item it's in to be removed from the nav bar.