PopViewController strange behaviour - ios

Due to a weird request which I tried to turn down but it didn't work, I had to override the navigationBar's back Button.
I have made a custom UINavigationController subclass and hacked the
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item method.
Here is my code:
#interface CustomUINavigationController ()
#end
#implementation CustomUINavigationController
#pragma mark - UINavigationBar delegate methods
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
[vc1 handleBackAction];
if (vc1.canPopVC == YES) {
[self popViewControllerAnimated:YES];
return YES;
} else {
return NO;
}
}
[self popViewControllerAnimated:YES];
return YES;
}
#end
All works fine, except when I pop a viewController programmatically. The app crashed every time when I wanted to perform a push after said pop. Turning NSZombie on, revealed that when popping a viewController programmatically, its parent viewController is deallocated.
At this point, making a custom backButton is not a option since it will lose the native iOS 7 swipe to popViewController feature.
Crash log:
*** -[ContactsDetailViewController performSelector:withObject:withObject:]: message sent to deallocated instance 0x1806b790

(My previous post was completely wrong. This is a complete rewrite with an appropriate solution.)
I had this behavior pop up when I chose to delete some code generating a warning when I was converting to ARC -- code that I thought was not being called.
Here's the situation:
If you shadow navigationBar:shouldPopItem: in a subclass of UINavigationController, then the current view controller will NOT be popped when the user touches the NavBar's BACK button. However, if you call popViewControllerAnimated: directly, your navigationBar:shouldPopItem: will still be called, and the view controller will pop.
Here's why the view controller fails to pop when the user touches the BACK button:
UINavigationController has a hidden method called navigationBar:shouldPopItem:. This method IS called when the user clicks the BACK button, and it is the method that normally calls popViewControllerAnimated: when the user touches the BACK button.
When you shadow navigationBar:shouldPopItem:, the super class' implementation is not called, and hence the ViewController is not popped.
Why you should NOT call popViewControllerAnimated: within your subclass' navigationBar:shouldPopItem::
If you call popViewControllerAnimated: within navigationBar:shouldPopItem:, you will see the behavior that you desire when you click the BACK button on the NavBar: You can determine whether or not you want to pop, and your view controller pops if you want it to.
But, if you call popViewControllerAnimated: directly, you will end up popping two view controllers: One from your direct call to popViewControllerAnimated:, and one from the call you added to within navigationBar:shouldPopItem:.
What I believe to be the safe solution:
Your custom nav controller should be declared like this:
#interface CustomNavigationController : UINavigationController <UINavigationBarDelegate>
{
// .. any ivars you want
}
#end
Your implementation should contain code that looks something like this:
// Required to prevent a warning for the call [super navigationBar:navigationBar shouldPopItem:item]
#interface UINavigationController () <UINavigationBarDelegate>
#end
#implementation CustomNavigationController
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
BOOL rv = TRUE;
if ( /* some condition to determine should NOT pop */ )
{
// we won't pop
rv = FALSE;
// extra code you might want to execute ...
} else
{
// It's not documented that the super implements this method, so we're being safe
if ([[CustomNavigationController superclass]
instancesRespondToSelector:#selector(navigationBar:shouldPopItem:)])
{
// Allow the super class to do its thing, which includes popping the view controller
rv = [super navigationBar:navigationBar shouldPopItem:item];
}
}
return rv;
}

I'm not 100% certain but I don't think you should actually be popping the view controller in that delegate method.
"should" delegate methods don't normally do something. They just assert whether something should or shouldn't be done.
Change your method to this...
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
if ([[self.viewControllers lastObject] isKindOfClass:[ViewController1 class]]) {
ViewController1 *vc1 = (ViewController1 *)[self.viewControllers lastObject];
[vc1 handleBackAction];
if (vc1.canPopVC == YES) {
return YES;
} else {
return NO;
}
}
return YES;
}
And see if it works.
All I have done is removed the popViewController calls.
EDIT - How to add a custom back button
In a category on UIBarButtonItem...
+ (UIBarButtonItem *)customBackButtonWithTarget:(id)target action:(#SEL)action
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:[UIImage imageNamed:#"Some image"] forState:UIControlStateNormal];
[button setTitle:#"Some Title" forState:UIControlStateNormal];
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
return barButtonItem;
}
Now whenever you want to set a custom back button just use...
UIBarButtonItem *backButton = [UIBarButtonItem customBackButtonWithTarget:self action:#selector(backButtonPressed)];

I would suggest a completely different approach.
Create a base class for the view controllers that you are pushing on the navigation stack. In the viewDidLoad method set your custom button as the leftBarButtonItem of the navigationItem and add a -backAction: which invokes the popViewControllerAnimated: method of the navigation controller.
That way you won't care about things like losing functionality of UINavigationController like the swipe to pop and you won't have to override the navigationBar:shouldPopItem: method at all.

You probably need to do [super shouldPop... instead of actual [self popViewControllerAnimated:YES];.
The reason being that the way UINavigationController implements stack is private, so you should mess with the method calls as little as possible.
Anyway, this looks like a hack. Moreover, the user will have no visual clue that you are blocking the navigation action. What's wrong with disabling the button via:
self.navigationController.navigationItem.backBarButtonItem.enabled = NO;

It's my fix to #henryaz answer for Xcode 11:
#interface UINavigationControllerAndNavigationBarDelegate : UINavigationController<UINavigationBarDelegate>
#end
#interface CustomNavigationController : UINavigationControllerAndNavigationBarDelegate
#end
// changed this method just a bit
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
BOOL shouldPop = // detect if need to pop
if (shouldPop) {
shouldPop = [super navigationBar:navigationBar shouldPopItem:item]; // before my fix this code failed with compile error
}
return shouldPop;
}

Related

Changing content of UINavigationController's Stack result in crash

I have a UINavigationController with 4 items:
(root)mainvc -> callerlistvc -> addcallerformvc -> verifycallervc (in that specific order)
When I am on the verifycallervc screen, if I press back, I want to go back to callerlistvc.
Here is the catch however, the back button should be a system button.. So.. as far as I know I cannot replace the action with a selector calling poptoviewcontroller:animated (only works on a custom uibarbuttonitem)
So then I thought of manipulating the stack (pretty interesting and challenging too!) So here is what I did...
So currently Im on the verifycallervc screen... and this gets called.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSMutableArray *allViewControllers = [self.navigationController.viewControllers mutableCopy];
__block UIViewController *mainvc = nil;
__block UIViewController *callerlistvc = nil;
__block UIViewController *addcallerformvc = nil;
[allViewControllers enumerateObjectsUsingBlock:^(UIViewController *vc, NSUInteger idx, BOOL *stop) {
if ([vc isKindOfClass:[MainVC class]]) {
mainvc = vc;
} else if ([vc isKindOfClass:[CallerListVC class]]) {
callerlistvc = vc;
} else if ([vc isKindOfClass:[AddCallerFormVC class]]) {
addcallerformvc = vc;
}
}];
[self.navigationController setViewControllers:#[ mainvc, callerlistvc, self]];
}
After I did that, I pressed back normally and was now on the callerlistvc... great.
Unfortunately when I press the button (push-segued to addcallerformvc)... it results in a crash EXC_BAD_ACCESS.
I also tried a different approach by first manipulating the variable callerlistvc like so before adding it in the setViewControllers method
callerlistvc = [[UIStoryboard storyboardWithName:#"main" bundle:nil] instantiateViewControllerWithIdentifier:#"CallerListVC"];
But the result is the same.
I have added breakpoints and it goes like this...
CallerListVC:
tappedShowAddCallerListButton
performSegueWithIdentifier
prepareForSegue // identifier string is correct, destinationVC is not nil
then AddCallerFormVC:
4. viewDidLoad
5. viewWillAppear // properties not nil
after that EXC_BAD_ACCESS occurs
How can I make this work?
The better approach in this case would be to use a custom UINavigationController class and extend the popViewControllerAnimated: method. In this method either call the super method or pop to the specific view controller (super class method) based on a check. That way you have your system nav buttons and also control where the tack should pop to.
Don't override anything in verifycallervc and instead do following with normal back on verifycallervc
Override viewWillAppear or viewDidAppear for addcallerformvc like this
- (void)viewDidAppear:(BOOL)animated
{
if (![self isBeingPresented]) {
[self.navigationController popViewControllerAnimated:YES];
}
}
Reference :
https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/RespondingtoDisplay-Notifications/RespondingtoDisplay-Notifications.html#//apple_ref/doc/uid/TP40007457-CH12-SW7
Note: not tested, don't have XCode right now....

Back button in standalone UINavigationBar with UINavigationController

I am trying to implement a push-up UINavigationBar, where the position of the navigation bar is attached to the contentOffset of the UIScrollView (similar to how safari works in ios7).
In order to get the dynamic movement working I am using a UINavigationBar created programatically and added as a subview of the UIViewController's view (it is accessible as self.navbar).
The UIViewController is within a UINavigationController hierarchy, so I am hiding the built-in self.navigationController.navigationBar at the top of -viewWillAppear:.
The problem I am trying to solve is to add a back button to this new standalone navbar. I would preferably like to simply copy the buttons or even the navigationItems from the navigationController and its hidden built-in navbar, but this doesnt seem to work
Is my only solution to set leftBarButtonItem on my standalone navbar to be a fake back button (when there is a backItem in the navController's navbar)? This seems a bit hacky, and I'd rather use the built backButton functionality.
Another way to do that, once you have your own UINavigationBar set, is to push two UINavigationItems on your navigationBar, causing back button to appear. You can then customize what happens when the back button is pressed.
Here's how I did that
1 - Some UINavigationItem subclass, to define extra-behavior / customization parameters
#interface MyNavigationItem : UINavigationItem
//example : some custom back action when 'back' is pressed
#property (nonatomic, copy) void (^onBackClickedAction)(void);
#end
2 - Then wire that into your UINavigationBarDelegate :
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
if ([item isKindOfClass:[MyNavigationItem class]]) {
MyNavigationItem *navItem = (MyNavigationItem *)item;
//custom action
if (navItem.backAction) {
navItem.backAction();
}
return YES;// return NO if you don't want your bar to animate to previous item
} else {
return YES;
}
}
You could adapt that scheme, calling your UINavigationController pop method on back action.
This is still hacky
Vinzzz' answer was a good solution. Here is my implementation, as the context was slightly different.
In the UIViewController's viewDidLoad method I setup my navbar's navigation items like this:
NSMutableArray* navItems = [#[] mutableCopy];
if (self.navigationController.viewControllers.count > 1)
{
NSInteger penultimateIndex = (NSInteger)self.navigationController.viewControllers.count - 2;
UIViewController* prevVC = (penultimateIndex >= 0) ? self.navigationController.viewControllers[penultimateIndex] : nil;
UINavigationItem* prevNavItem = [[UINavigationItem alloc] init];
prevNavItem.title = prevVC.title;
[navItems addObject:prevNavItem];
}
UINavigationItem* currNavItem = [[UINavigationItem alloc] init];
... <Add any other left/right buttons to the currNavItem> ...
[navItems addObject:currNavItem];
[self.navbar setItems:navItems];
...where self.navbar is my floating stand-alone UINavigationBar.
I also assign the current view controller to be self.navbar's delegate, and then listen for the -navigationBar:shouldPopItem: event that is triggered when the back button is pressed:
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
if (navigationBar == self.navbar)
{
[self.navigationController popViewControllerAnimated:YES];
return NO;
}
return YES;
}
(If you return YES, it will crash when a swipe gesture is used in ios7).

How to switch from one tab to another and back?

I've got a little problem on navigating through my views.
Here is my configuration :
Ive got 1 Tabbar Controller with 2 relationship segues to 2 simple views embedded inside a navigation controller.
Now i want to navigate from view controller 1 to view controller 3 and i also want to show the correct tab selected inside the tabbar. And if i come from view controller 1, i also want that the back button redirects me the the previous tab. I tried something with a segue connected between that views, but if i do so, it just pushes the view controller onto the navigation stack but not changes the tab. So my question now is, what is the best way of managing this Problem
Screenshot:
Here's a way to do it. But I'm posting this really to illustrate why you shouldn't do it.
I'm using VC2's and VC3's view tag property to pass navigation data around, which has the effect of tightly coupling all three objects.
I override back bar button for the VC1->VC3 context. You lose consistency.
VC3->VC2 provides back animation. VC3->VC1 has no animation as it flips from one tab to another. More inconsistency
VC1->VC3, tap Tab Item 2 transitions to VC2. UI confusion.
Anyway if you still want to do this...
.
ViewController1
Has a "jump to VC3" button, wire up to jumpToVC3:
//ViewController1.m
#import "ViewController1.h"
#implementation ViewController1
- (IBAction)jumpToVC3:(id)sender {
NSArray* viewArray = [[[self.tabBarController viewControllers] objectAtIndex:1] viewControllers];
[[[viewArray lastObject] view] setTag:1];
[self.tabBarController setSelectedIndex:1];
}
#end
"jumptToVC3" switches us to tab 2 and sets the frontmost view's view tag property to 1. IF the frontmost view is VC2, this triggers an immediate segue to VC3. If the frontmost view is VC3, this sets up the back button correctly. If other View Controllers get added to this stack, this navigation will break.
ViewController2
Has a "move to VC3" button, wired to a storyboard segue to VC3 "toVC3"
// ViewController2.m
#import "ViewController2.h"
#import "ViewController3.h"
#implementation ViewController2
//we use the view.tag property as a switch:
//0 = do nothing
//1 = segue to VC3
//2 = go to tab 0
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.view.tag ==1){
[self performSegueWithIdentifier:#"toVC3" sender:self];
} else if (self.view.tag == 2){
[self.tabBarController setSelectedIndex:0];
}
self.view.tag = 0;
}
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if (self.view.tag ==1) {
[[segue.destinationViewController view] setTag:1];
} else {
[[segue.destinationViewController view] setTag:0];
}
self.view.tag = 0;
}
- (void) viewWillDisappear:(BOOL)animated
{
self.view.tag = 0;
}
#end
ViewController3
Overrides the back button if it's view.tag is set to 1. If you want both context's back buttons to be consistent, you will need to override for the default behaviour as well. You will not be able to get a standard back button look for this override behaviour.
// ViewController3.m
#import "ViewController3.h"
#implementation ViewController3
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.view.tag == 1) {
self.navigationItem.leftBarButtonItem =
[[UIBarButtonItem alloc] initWithTitle:#"0.0"
style:UIBarButtonItemStyleBordered
target:self
action:#selector(goBack:)];
}
self.view.tag = 0;
}
- (IBAction)goBack:(id)sender {
[[[[self.navigationController viewControllers]
objectAtIndex:0] view] setTag:2];
[self.navigationController popToRootViewControllerAnimated:YES];
}
#end

UINavigationBar only calling some delegates

I'm adding a UINavigationItem with a save button to the right side, and a back button on the left side to my UINavigationBar. I am setting my view controller to be the delegate, and the navigation bar is calling some delegate functions, but not -(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item.
In ISStudioReviewViewController.h:
#interface ISStudioReviewViewController : UIViewController <UINavigationBarDelegate, UITextViewDelegate>
#property (weak, nonatomic) IBOutlet UINavigationBar *header;
#end
In ISStudioReviewViewController.m:
-(void)viewDidLoad {
[super viewDidLoad];
...
...
UINavigationItem *navHeader = [[UINavigationItem alloc] initWithTitle:#"Compose Review"];
[navHeader setHidesBackButton:NO];
UIBarButtonItem *actionButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave
target:self
action:#selector(saveReview)];
[navHeader setRightBarButtonItem:actionButton];
[header setDelegate:self];
[header pushNavigationItem:navHeader animated:NO];
}
-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
NSLog(#"Popping item");
....
return YES;
}
-(BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPushItem:(UINavigationItem *)item
{
NSLog(#"Should push item");
return YES;
}
-(void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item
{
NSLog(#"Pushing item!");
}
In my logs I can see that the shouldPush and didPush delegate methods are being called correctly, and my back button is being displayed. However clicking on the back button is not calling the delegate method, and Popping item is never logged.
I should note that the save button actionButton does properly call it's target without issue. I have tried not adding this button, and seeing if that is somehow causing the back button to fail, but no luck.
I am using ARC and targeting iOS 6.0.
The Apple Documentation states that you are not allowed to do this delegate.
In addition, a navigation controller object automatically assigns itself as the delegate of its UINavigationBar object and prevents other objects from changing that relationship.
One/The? way to do what you want is to put in your own back-button.
In that Method you do your tests and call
[self.navigationController popViewControllerAnimated:true];
if the user is allowed to go back.

How to Dismiss a Storyboard Popover

I've created a popover from a UIBarButtonItem using Xcode Storyboards (so there's no code) like this:
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?
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).
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;
...
}
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
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:
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...
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
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

Resources