I've currently have an iPad app with a UIToolbar containing two UIBarButtonItems, each of which is connected to a popover segue.
When the user touches either of the UIBarButtonItems, the popover is created rather than toggled. This creates multiple, overlapping popovers. I've been able to close the previously created popover using the following code
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// First close the preferences popover if it's open per Apple guidelines
if ([self.popoverA.popoverController isPopoverVisible]) {
[self.popoverA.popoverController dismissPopoverAnimated:YES];
}
if ([self.popoverB.popoverController isPopoverVisible]) {
[self.popoverB.popoverController dismissPopoverAnimated:YES];
}
... code to manage segues ...
}
I also have UIButtons which create popover segues which behave normally. Due to this behavior of the popovers associated with UIBarButtonItems, my app is being rejected. Does someone have any suggestions or any code samples of a UIToolbar with multiple UIBarButtonItems that work correctly? The popovers do dismiss when the user touches outside the window,
This is the proper way to do what you need to do:
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"SurveyListPopover"]) {
if (self.surveyListPopover == nil) {
return YES;
}
return NO;
}
return YES;
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"SurveyListPopover"]) {
// Assign popover instance so we can dismiss it later
self.surveyListPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
}
}
This ensures that the segue will be cancelled if an instance of the popover has already been displayed. You just need to make sure your popover object has an identifier in the storyboard.
By the time you get messaged in -prepareForSegue:sender:, it's too late to cancel a segue.
In order to do this efficiently, you should create segues to your popovers from the view controller itself instead of the bar buttons so that they can still be programmatically executed. Now wire the UIBarButtonItems up to some methods that will conditionally present or dismiss the popover.
- (IBAction)showPopoverA
{
if (self.popoverA.popoverController.popoverVisible)
[self.popoverA.popoverController dismissPopoverAnimated:YES];
[self performSegueWithIdentifier:#"ShowPopoverA"];
}
Combination of both made it for me
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"showPopover"]) {
self.tableOfContentsPopoverController = [(UIStoryboardPopoverSegue*)segue popoverController];
}
}
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"showPopover"]) {
if (!self.tableOfContentsPopoverController.popoverVisible) {
return YES;
}
return NO;
}
return YES;
}
Related
I'm trying to create a manual segue, which happens only if there is a certain conidition (a successful login).
The steps I have followed are these:
1) I've created a button in source viewcontroller, and i connected the button with a drag with the destination viewcontroller.
2) I set the identifier for the segue, calling it login_ok
3) I've created a -(IBAction)buttonPressed:(id)sender
Now, I tried to make an example, to see if the work or not. I wrote:
- (IBAction)buttonPressed:(id)sender
{
BOOL test = false;
if(test == true)
{
[self performSegueWithIdentifier:#"login_ok" sender:self];
}
Theoretically, the segue should not work, because i set test=false, and i said the segue should work only if test=true.
But the segue works and makes me go to the destination viewcontroller.
Why?
If you connected the button with a drag to the destination controller, it should segue anyway.
You need to connect the segue between screens and not from the button to the screen. Then, you can control your segue from the code.
You should define your segue between controllers not from button to the controller. Defining segue between view controllers is more useful than defining segue from any component to a view controller, because when you want to use a condition for any action, you can use these two methods effectively
[self performSegueWithIdentifier:#"segueID" sender:self];
and
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
For example:
- (IBAction)buttonPressed:(id)sender
{
BOOL test = YES;
if(test == YES)
[self performSegueWithIdentifier:#"login_ok" sender:self];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"login_ok"])
{
YourNextController *controller = [segue destinationViewController];
controller.welcomeText = #"Hi! Test is true congratulations";
}
}
Especially passing data from controller to another one, this way is always useful.
Then I call this method I want to have segue. Is it right?
- (void)showMapViewController {
[self performSegueWithIdentifier:#"MapViewController" sender:self];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"MapViewController"])
{
UINavigationController *navigationController = segue.sourceViewController;
LoginViewController *loginViewController = [[navigationController viewControllers] objectAtIndex:0];
[loginViewController performSegueWithIdentifier:#"MapViewController" sender:self];
}
}
You have to create segue in storyboard and give a unique identifier. It can be perform automaticaly if you set this on some button action, e.g. if you create a segue on button action an want to push a navigation controller. When ever that button will be push segue will be perform automaticlay. In this case if you want to do some custom coding to set some properties etc. then you should implement this delegate method
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"segueIdentifier"])
{
// Here you can get source and destination view controllers and can perform some custom tasks.
}
}
You can also perform segue by code if you want to, for that you have to call this method.
[self performSegueWithIdentifier:#"segueidentifer" sender:self];
For this you have to create a segue in storyboard by clt+drag from one view controller to other(source -> destination) and give an identifier.
I got a problem with a universal app that were initial design for the iPhone and I used a 3rd party class for a popup on the iPhone. on the iPad I want to use the Apple provided popup. My problem is my code is written so that the View Controller for the popup already is initialized in the viewDidLoad of the class containing the popup, and when seguing - a new instance of that class is allocated. Is there a way to pass it the bckMusicPlayer that´s already been allocated in prepareForSegue?
#property (nonatomic,strong) MJDetailViewController *bckMusicPlayer; //Already initialized when prepareForSegue get´s called. I want to segue to this object.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
NSLog(#"prepare for segue");
if ([segue.identifier isEqualToString:#"Show iPad Player"]) {
if ([segue isKindOfClass:[MJDetailViewController class]]) {
}
}
}
edit: I got it working passing it the player (AVAudioPlayer) object from bckMusicPlayer
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"Show iPad Player"]) {
NSLog(#"class: %#",[segue class]);
if ([segue.destinationViewController isKindOfClass:[MJDetailViewController class]]) {
NSLog(#"prepare for segue");
MJDetailViewController *destinationVC = segue.destinationViewController;
destinationVC.player = self.bckMusicPlayer.player;
}
}
}
Can't you go the other way round? Why do you have to create an instance of MJDetailViewController? Let the segue do it for you and then in prepareForSegue: you grab the destination controller and pass it the required data. Don't fight the framework.
Alternatively you could override:
- (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
And prevent the segue in case it is targeting MJDetailViewController and manually transfer to your controller and present it.
No, you can't do that. Segues (other than unwinds) always instantiate new controllers. You either need to not instantiate it in the viewDidLoad, or move to the new controller in code rather than using a segue.
I have a button which leads to a popOver, all created in Interface Builder. The popOver is closed when I press somewhere outside of it, but I would also like to implement a button within the popOver which does that.
I found a solution by Giorgio Barchiesi dating back to 2011, however I fail to implement it. Here's his solution:
In the implementation file of the source view controller:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue destinationViewController] isKindOfClass:[MyDestViewController class]]) {
MyDestViewController* viewController = (MyDestViewController*)[segue destinationViewController];
UIStoryboardPopoverSegue* popoverSegue = (UIStoryboardPopoverSegue*)segue;
[viewController setPopoverController:[popoverSegue popoverController]];
}
}
In the header file of the destination view controller:
#property (weak, nonatomic) UIPopoverController* popoverController;
In the implementation file of the destination view controller:
#synthesize popoverController;
Same file, whenever you want to dismiss the popover:
[popoverController dismissPopoverAnimated:YES];
i could call the last function when the button is pressed.
My problem is that XCode gives me an error on the [viewController setPopoverController:[popoverSegue popoverController]] line: ARC Semantic Issue: No known class method for selector 'setPopOverController'
What did I miss to implement?
Here is the method I use:
Open your storyboard file, select the segue arrow and open the Attributes Inspector (Option - Command - 4) and identifier fill in a sensible name, like "myPopoverSegue".
In your Source View Controller define a variable right after #implementation :
#implementation ViewController
{
__weak UIPopoverController *myPopover;
}
Then, again in the Source VC:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if ([segue.identifier isEqualToString:[dict objectForKey:#"myPopoverSegue"]]) {//#"segue" is your segue name. You can use isKindOfClass as you do currently, I prefer this method.
myPopover = [(UIStoryboardPopoverSegue *)segue popoverController];
}
}
-(void)closePopover{
[myPopover dismissPopoverAnimated:YES];
}
In the end of your Source VC's viewDidLoad method write:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(closePopover) name:#"popoverShouldDismiss" object:nil];
Finally, whenever you want to dismiss the popover:
[[NSNotificationCenter defaultCenter] postNotificationName:#"popoverShouldDismiss" object:nil];
Hope this helps!
This way you will also be able to change the segue to a different controller without changing your code.
You can add the delegate < UIPopoverControllerDelegate > to your class and override the delegate method:
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
return NO;
}
This will prevent the popover to be dismissed when user presses anywhere on screen.
Now you can dismiss your popover inside the button's selector method by using:
[popoverController dismissPopoverAnimated:YES];
In iOS 8 it's really easy. Just call
[self dismissViewControllerAnimated:YES completion:^{}];
Pop overs are regular presentation controllers, so it's more or less the exact same thing as a modal view controller.
Try this code
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue destinationViewController] isKindOfClass:[MyDestViewController class]])
{
MyDestViewController* viewController = (MyDestViewController*)[segue destinationViewController];
UIStoryboardPopoverSegue* popoverSegue = (UIStoryboardPopoverSegue*)segue;
popoverSegue.popoverController=[[UIPopoverController alloc] initWithContentViewController:viewController];
[popoverSegue.popoverController setPopoverContentSize:CGSizeMake(viewController.view.frame.size.width, viewController.view.frame.size.height)];
popoverSegue.popoverController.delegate=self;
[viewController setPopoverController:popoverSegue.popoverController];
}
}
I hope it helps you.
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