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];
}
}
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;
}
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!
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.
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;
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.
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
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;
}
Related
I have view and a lot of segues from different other views to it, is there any property to check which segue does call this view?
I am not sure tht you can check it by just segue name.
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"view1"])
{
[segue.destinationViewController setVariable:variable];
}
}
Create this variable in destinationViewController.
Then check value of "variable".
You make use of the the following function in your viewcontroller,In which making use of the segue identifier you are able to identify which view has called the upcoming view.
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"view1"])
{
NSLog(#" called by view1");
[segue.destinationViewController setVariable:variable];
}
}
Create a #property in your destination ViewController
Assign segue identificator to it in -(void)prepareForSegue method:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.destinationViewController respondsToSelector:#selector(setMyIdentifier)]) {
[segue.destinationViewController setMyIdentifier:segue.identifier];
}
}
Handle self.myIdentifier property in -viewDidLoad of your destination ViewController
I have an Facebook enabled, iOS 5 app that uses a storyboard and segue based navigation and am confused on how to implement "iOS native deep linking." The example code at Improving App Distribution on iOS just displays a UIAlertView but I am trying to initiate two consecutive seque operations.
For purposes of this question, I've simplified the application to three view controllers: MYCategoryTableViewController, MYItemsTableViewController and MYItemViewController. In the normal flow, the application opens to MYCategoryTableViewController, which displays a table of categories. When a category is selected by the user, there is a segue to MYItemsTableViewController which displays a table of items for that selected category. Lastly, when an item is selected, there is a segue to MYItemViewController which displays an item detail view.
The prepareForSegue from MYCategoryTableViewController sets a property on the destination view controller that represents that category:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"ITEMS_SEGUE"]) {
MYItemsTableViewController *vc = [segue destinationViewController];
MYCategory *mycategory = [self.fetchedResultsController objectAtIndexPath:[self.tableView indexPathForSelectedRow]];
vc.mycategory = mycategory;
}
}
The prepareForSegue from MYItemsTableViewController sets a property on the destination view controller that represents that category:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([[segue identifier] isEqualToString:#"ITEM_SEGUE"]) {
MYItemViewController *vc = [segue destinationViewController];
MYItem *myitem = [self.fetchedResultsController objectAtIndexPath:[self.tableView indexPathForSelectedRow]];
vc.myitem = myitem;
}
}
Question: I know that I need to implement something in application:openURL, but not sure what to do next. Assume the incoming URL gives identifiers to lookup the MYCategory and MYItem objects. I found performSegueWithIdentifier but not sure how that interacts with prepareForSegue and how I set my model objects on the destination view controllers.
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
// get "target_url" from incoming url
// and parse out MYCategory and MYItem identifiers
// something like this???
[self.window makeKeyAndVisible];
[self.window.rootViewController performSegueWithIdentifier:#"ITEM_SEGUE" sender:self];
return [facebook handleOpenURL:url];
}
Update: Selecting programmatically a cell on a tableview doesn't perform associated segue has given me an idea. Maybe I just save off the url from application:openURL: and let MYCategoryTableViewController load naturally. Then during viewWillAppear, call tableView selectRowAtIndexPath and then performSegueWithIdentifier to transition to MYItemsTableViewController. Repeat the same pattern in MYItemsTableViewController, but clear out the url before the performSegueWithIdentifier call.
Here is what I got working. In MYAppDelegate, I captured a string represented the id of the deep link.
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
NSString *deepLinkId;
// more code that parses url
// only deep link if MYCategoryTableViewController is active controller
UIViewController *rootContoller = self.window.rootViewController;
if ([rootContoller isKindOfClass:[UINavigationController class]]) {
UINavigationController *navController = (UINavigationController *)rootContoller;
if ([navController.topViewController isKindOfClass:[MYCategoryTableViewController class]]) {
self.deepLinkId = deepLinkId;
}
}
}
Then, when MYCategoryTableViewController loads, call selectRowAtIndexPath and then performSegueWithIdentifier.
- (void)processDeepLink {
if (_appDelegate.deepLinkId) {
MYItem *myitem = [MYItem lookupById:_appDelegate.deepLinkId inManagedObjectContext:_appDelegate.dataDocument.managedObjectContext];
if (myitem) {
NSIndexPath *indexPath = [self.fetchedResultsController indexPathForObject:myitem.mycategory];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle];
[self performSegueWithIdentifier:#"ITEMS_SEGUE" sender:self];
}
}
}
And in when MYItemViewController loads, a similar flow.
if (_appDelegate.deepLinkId) {
MYItem *plate = [MYItem lookupById:_appDelegate.deepLinkId inManagedObjectContext:_appDelegate.dataDocument.managedObjectContext];
NSIndexPath *indexPath = [self.fetchedResultsController indexPathForObject:plate];
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionMiddle];
_appDelegate.deepLinkId = nil;
[self performSegueWithIdentifier:#"ITEM_SEGUE" sender:self];
}
I also had to observe UIApplicationDidBecomeActiveNotification for the use case when the application was already open.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(processDeepLink)
name:UIApplicationDidBecomeActiveNotification
object:nil];
I'm using the template master detail application. I have added a modal segue from SplitViewController and given it the identifier "DisplayLoginView".
I call the following from my detailViewController:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:YES];
[self.splitViewController performSegueWithIdentifier:#"DisplayLoginView" sender:self.splitViewController];
}
I also have the prepareForSegue method defined in detailViewController:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSLog(#"Source Controller = %#", [segue sourceViewController]);
NSLog(#"Destination Controller = %#", [segue destinationViewController]);
NSLog(#"Segue Identifier = %#", [segue identifier]);
if ([segue.identifier isEqualToString:#"DisplayLoginView"])
{
PrometheusLoginViewController *loginViewController = (PrometheusLoginViewController *)segue.destinationViewController;
loginViewController.delegate = self;
}
}
Any idea on why it's not called?
You're asking the splitViewController to perform the segue, but you're defining prepareForSegue in the detailViewController. They need to be on the same object for prepareForSegue to be triggered.
I implemented a search option in my app but when I search something, I can't push a detail controller from the search display controller's table view.
Is there a way to do that?
Thank you so much!
We'll assume you have a UISearchBarDelegate method in a UIViewController called SearchViewController. To push (DetailViewController*) dvc, implement these methods in SearchViewController:
#pragma mark - Search bar delegate
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if (searchTextMatchedSomeTarget) {
textForDetailView = searchText;
[self performSegueWithIdentifier:seguePushDetailView sender:self];
}
}
#pragma mark - View lifecycle
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:seguePushDetailView]) {
dvc = (DetailViewController *)[segue destinationViewController];
dvc.delegate = (id)self;
dvc.searchText = textForDetailView;
}
}
#pragma mark - Detail view controller protocol delegate
- (void)DetailViewControllerDidFinish:(DetailViewController *)controller
{
NSString *somethingBackFromDetailView = controller.backToSearchView;
}
UIDetailViewController declares a protocol with the "did finish" method and properties of whatever type you like, both to receive data from SearchViewController and to send back any data after any detail processing. The segue is added in Xcode IB by control dragging from UISearchViewController to UIDetailViewController. NB: the segue source and destination are the view controllers themselves. That way they can be invoked programmatically as opposed to automatic invocation on a tap event.
I have a problem with the Storyboard in xCode 4.2. Is it posible to check, if a boolean is true to load the next view or if it is false to do not load the new view an stay in the actual view.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"toCategory"] && ([[codeField text] isEqualToString:#"1234"]))
{
QuestInfoController *qicontroller = [segue destinationViewController];
}
else
{
[super prepareForSegue:segue sender:self];
return;
}
}
Yes, but you don't do it quite like this. You'd use a custom method or check for your boolean value - then you'd use performSegueWithIdentifier: to force the transition based on the outcome.
I wrote another post about it here. It demonstrates how to wire a few buttons on a page to push the next view. Note that in the buttonPressed: method would be where you do your check.