I was wondering how I would be able to update a view controller property of the presenting view controller from a method called inside the modal view controller? Right now, the only way I could think of is using NSNotificationCenter, and while this works for Dictionary items, I couldn't figure out how to use it for a custom object.
For example, my presenting view controller HomewViewController has a Parse PFObject called homeSelections, which the could be updated in the modally presented ModalViewController's property newSelections (and also a PFObject) . After the user makes her selections, I would like HomeViewController's homeSelections to also have the latest data passed from the modal view controller.
Help is appreciated - thanks.
Update 1: here is what I have done now (note that I am using a stripped down example to test things out)
In ViewController (this is the parent/presenting view controller)
#interface ViewController ()
#property (strong, nonatomic) NSArray *totalRamen;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.totalRamen = #[#"ramen1", #"ramen2", #"ramen3", #"moot"];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(#"self.totalRamen: %#", self.totalRamen);
NSLog(#"Done");
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"showModal"]){
ModalViewController *destinationVC = (ModalViewController *)segue.destinationViewController;
destinationVC.passedRamen = self.totalRamen;
}
}
- (IBAction)showModalAction:(UIButton *)sender
{
ModalViewController *destinaionViewController = [[ModalViewController alloc] init];
destinaionViewController.selectionCallback = ^(id selectedItem) {
self.totalRamen = (NSArray *)selectedItem;
NSLog(#"self.totalRAmen %#", self.totalRamen);
NSLog(#"done");
};
[self performSegueWithIdentifier:#"showModal" sender:self];
}
In ModalViewController (this is the presented/modal view controller)
#interface ModalViewController : UIViewController
#property (strong, nonatomic) NSArray *passedRamen;
- (IBAction)dismissModal:(UIButton *)sender;
typedef void(^CallbackBlock)(id value);
#property (nonatomic, copy) CallbackBlock selectionCallback;
#end
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
NSLog(#"Passed ramen %#", self.passedRamen);
NSLog(#"Done");
self.passedRamen = #[#"moot is awesome"];
NSLog(#"new ramen: %#", self.passedRamen);
NSLog(#"%#", self.selectionCallback); //nil here
//call back
CallbackBlock selectionCallback = self.selectionCallback;
if (selectionCallback){
selectionCallback(self.passedRamen); //I want to send the newly updated self.passedRamen back
} else {
NSLog(#"No show"); //Means if isn't called
}
}
- (IBAction)dismissModal:(UIButton *)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Pass a callback block into the presented view controller. This way the presented view controller doesn't know anything about the view controller presenting it. Much more flexible because now anybody can present your view controller, they just pass it a block!
PresentingViewController
PresentedViewController *vc = [[PresentedViewController alloc] init]; //or get your existing one
vc.selectionCallback = ^(id selectedItem) {
//update selected items here
};
//present vc here
PresentedViewController
typedef void(^CallbackBlock)(id value);
#property (nonatomic, copy) CallbackBlock selectionCallback;
- (void)somethingWasSelected:(id)selectedItem {
CallbackBlock selectionCallback = self.selectionCallback;
if (selectionCallback) selectionCallback(selectedItem);
}
Beware of retain cycles. This block is being retained by the presented view controller so references to the presented view controller in the block without weakifying it first will create a leak. More info on this can be found here.
After the user makes her selections, I would like HomeViewController's
homeSelections to also have the latest data passed from the modal view
controller.
The easy way is to avoid having ModalViewController update HomeViewController at all. Turn the communication around -- have HomeViewController query ModalViewController and update itself when the modal is dismissed.
HomeViewController already depends on ModalViewController -- it has to know about ModalViewController in order to present it. So there's no harm in having it also know how to read the newSelections property out of ModalViewController at the appropriate time. ModalViewController, on the other hand, has no need to know anything about where its information goes. It doesn't need to know about HomeViewController in order to do its job. If you avoid telling ModalViewController anything about HomeViewController, you can easily use ModalViewController from some other view controller should the need for that ever arise. More importantly, you avoid ever needing to update ModalViewController if HomeViewController changes.
For example, your HomeViewController might look (in part) like this:
- (void)showModalViewController
{
self.modalViewController = [[ModalViewController alloc] init]; // or otherwise get the modal controller
[self presentViewController:self.modalViewController
animated:YES
completion:];
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
{
self.homeSelections = self.modalViewController.newSelections;
[super dismissViewControllerAnimated:flag completion:completion];
self.modalViewController = nil;
}
Related
I have two ViewControllers, which aren`t (directly) connected with a Segue. But I want to change a String in the second VC, which i get in the first one. My try was:
#import "secondViewController.h"
#interface firstViewController ()
#property NSString* originalString;
#end
#implementation firstViewController
-(void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
secondViewController* svc = [secondViewController new];
svc.anotherString = self.originalString;
}
But it dosent work, because I've only created a instance of the second VC, so the value was not saved. Also I can`t use the Storyboard ID, because I use Xcode 5.
I have a menuVC from which you can get to the firstVC and the secondVC. And from the firstVC I can go back (with the navigationbackbarbutton) to the menu. so: menu->firstVC->menu. menu->secondVC->...->menu
My try with StoryboardID:
-(void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
secondViewController* svc =[[UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil] instantiateViewControllerWithIdentifier:#"secondVCSrorybradID"];
svc.anotherString = self.originalString;
}
you can pass the string to second view controller with this code.
secondViewController* svc =[self.storyboard instantiateViewControllerWithIdentifier:#"Your Second VC's Storyboad ID"];
svc.anotherString = self.originalString;
[self presentViewController:svc animated:YES completion:nil];
//you have to create anotherString property in second View Controller's .h File.
now you can get the string of originalString to second VC. Now you can get this value back second VC to first VC.
hope this helps you.
You should pass your data successively through your UIViewControllers navigation. If, for example, you have a navigation like FirstVC > SecondVC > ThirdVC :
In your FirstVC.m, use :
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
((SecondVCClass*) segue.destinationViewController).secondVCString = _firstVCString;
}
With secondVCString being a #property in your second ViewController.
In your SecondVC.m, use :
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
((ThirdVCClass*) segue.destinationViewController).thirdVCString = _secondVCString;
}
With of course thirdVCString being a #property in your third ViewController.
Edit:
As you updated your question, here is what I suggest :
In your MenuVC, add this :
#property (nonatomic, weak) NSString *importantString;
In your FirstVC and SecondVC, add this :
#property (nonatomic, weak) MenuVCClass *menu;
When you push to FirstVC or SecondVC, use prepareForSegue to set the destination's view controller menu property to your menu :
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"FirstSegue"])
((FirstVC*) segue.destinationViewController).menu = self;
else if ([segue.identifier isEqualToString:#"SecondSegue"])
((SecondVC*) segue.destinationViewController).menu = self;
}
In FirstVC or SecondVC, you can change the NSString value from your menu using _menu.importantString = #"";
You should never use new to create a view controller.
If you're using storyboards but not using segues, you can still create a view controller from the storyboard and invoke it.
Use the method instantiateViewControllerWithIdentifier: to create an instance of the target view controller. The set the properties you want to set, and finally make a call to display it (present it modally, push it onto the navigation stack, or whatever is appropriate for your program.)
I'm new to iOS programming and I'm facing a problem
I'm having a problem with custom delegate.
I'm trying to make a simple custom where it return data to the previous view controller and pop the current view controller.
I have 2 navigation view controller
1 - main view controller
2 - Adding
and here is the protocol that is written in the adding view controller
#protocol AddingDelegate <NSObject>
#required
-(void)setInformation:(Adding *)controller withObject:(Conference *)info;
and here is the where I called it in adding view controller
-(IBAction)addingConference
{
NSLog(#"Adding Button Pressed");
conferenceObject = [[Conference alloc]init];
conferenceObject.name = [NameTX text];
conferenceObject.city = [CityTX text];
conferenceObject.description = [Dectription text];
NSMutableArray *info = [[NSMutableArray alloc] init];
[info addObject:conferenceObject];
[self.delegate setInformation:self withObject:conferenceObject];
NSLog(#"adding Conference method is done");
}
I wrote the delegate at the interface in the main view controller
#interface MainViewController : UITableViewController <AddingDelegate>
#end
and here where I declared the delegate method
-(void)setInformation:(Adding *)controller withArray:(NSMutableArray *)info
{
NSLog(#"in the main view at the delegate");
[self.navigationController popToRootViewControllerAnimated:YES];
NSLog(#"Should be popped right now");
}
and this is the prepare for segue method
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"AddObject"]) {
UINavigationController *navigation = segue.destinationViewController;
Adding *addingViewController = [[navigation viewControllers]objectAtIndex:0];
addingViewController.delegate = self;
}
}
now the problem is when I push the adding on top of the stack and then fill the information and press done the adding view controller doesn't pop to show main view controller.
I tried to log everything and the logs from the main view controller doesn't show .
Please help me
What I notice here is that in the implementation of prepareForSegue:sender: the segue's destinationViewController is a navigation controller. This makes me think that your segue is not pushing the AddingController on the current navigation stack but it's presenting a new one instead. This means the new navigation controller containing the AddingController is presented modally and as such, when you try to pop the navigation stack nothing seems to happen because you're operating on the wrong navigation stack. If that is the case you have two options: 1. change [self.navigationController popToRootViewControllerAnimated:YES]; for [self dismissViewControllerAnimated:YES completion:nil]; or 2. change the segue to be a push segue instead of a modal segue and point the segue directly to the AddingController.
In Adding.m
#class Adding;
#protocol AddingDelegate
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller;
#end
#interface Adding : UIViewController
#property (weak, nonatomic) id <AddingDelegate> delegate; // have you forgot this one
#end
and use
[self dismissViewControllerAnimated:YES completion:nil];
You need dismiss if you want get back to previous screen and make sure you have added Navigation controller
I am presenting a modal view using a storyboard segue set as Form Sheet.
The problem is, when I rotate the iPad after this view is displayed, the view is removed from the view/dismissed.
I have no idea why. It only seems to occur when starting in Portrait then rotating to Landscape.
If I start in Landscape then show the view then rotate it stays on the screen fine.
Any ideas?
EDIT ----
It also seems that full screen modal views are also dismissed after rotation!
There's nothing special going on in the presentation code, this is a full screen modal:
EditViewController *editView = [self.navigationController.storyboard instantiateViewControllerWithIdentifier:#"editViewController"];
editView.delegate = self;
editView.image = image;
editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:editView animated:YES completion:nil];
This happens on both iOS 6 and iOS 7
EDIT 2 ----
Forgot to mention, i'm presenting the modal from the left/master view controller of a UISplitViewController
late, but what it worked for me was just before
[self presentViewController:aController animated:YES completion:nil];
dismiss the master controller, adding this lines
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAutomatic];
and then present your controller
Get rid of: editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
That will solve the issue you are experiencing. Modal view controllers presented on iPad as a Form Sheet do not rotate correctly using that transition style.
its really hard to get the cause how and why this is happening as i found that this also happen with UIPopover also as when you rotate it UIPopover hide because ???
So if you want to keep your view then just call again your controller after rotation will do fine user experience
This is not a bug its a limitation on UISplitViewController. The problem exists when the masterViewController (which is a UIPopoverController) is able to be dismissed. Heres how it works with the assumption that your app does allow the masterViewController to be dismissed in portrait and does not allow in landscape.
In portrait while the masterViewController is visible, if you were to present a modal from a viewController in the masterViewController and then rotate to landscape, the modal would disappear in iOS7 and the app would not rotate in iOS8. iOS8 introduces a condition to prevent the bad experience of iOS7. iOS7 losses the modal in the process of moving the masterViewController from the popoverController to a contained viewController in the splitViewController.
The modal needs to be presented from the splitViewController and not from the masterViewController. The only problem with this is the modal gets presented below the masterViewController in portrait. My solution is to dismiss the masterViewController and then present the modal.
There are several ways to achieve this result depending on how complex your code needs to be. Here's how I do this in my app.
I first subclass UISplitViewController in order to have a reference to the popoverController. I use delegate forwarding in order to access the delegate methods internally and externally. Heres the .h
// MainSplitViewController.h
#import <UIKit/UIKit.h>
#interface MainSplitViewController : UISplitViewController
#property (nonatomic, weak, readonly) UIPopoverController* primaryColumnController;
#end
And the .m
// MainSplitViewController.m
#import "MainSplitViewController.h"
#interface MainSplitViewController () <UISplitViewControllerDelegate>
#property (nonatomic, weak) id<UISplitViewControllerDelegate> externalDelegate;
#property (nonatomic, weak) UIPopoverController* primaryColumnController;
#end
#implementation MainSplitViewController
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
#pragma mark - Split View Controller Delegate
- (void)splitViewController:(UISplitViewController *)svc popoverController:(UIPopoverController *)pc willPresentViewController:(UIViewController *)aViewController {
self.primaryColumnController = pc;
if ([(id)self.externalDelegate respondsToSelector:_cmd]) {
[self.externalDelegate splitViewController:svc popoverController:pc willPresentViewController:aViewController];
}
}
#pragma mark - Delegate Forwarder
- (void)setDelegate:(id<UISplitViewControllerDelegate>)delegate {
[super setDelegate:nil];
self.externalDelegate = (delegate != self) ? delegate : nil;
[super setDelegate:delegate ? self : nil];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [super respondsToSelector:aSelector] || [delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [delegate respondsToSelector:aSelector] ? delegate : [super forwardingTargetForSelector:aSelector];
}
#end
Next I create a class extension on UIViewController
// UIViewController+Popover.h
#import <UIKit/UIKit.h>
#interface UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController;
#end
And the .m
// UIViewController+Popover.m
#import "UIViewController+Popover.h"
#import "MainSplitViewController.h"
#implementation UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController {
UIViewController* viewController = self;
if ([self.splitViewController isKindOfClass:[MainSplitViewController class]]) {
viewController = self.splitViewController;
MainSplitViewController* mainSplitViewController = (MainSplitViewController *)self.splitViewController;
if (mainSplitViewController.primaryColumnController.popoverVisible) {
[mainSplitViewController.primaryColumnController dismissPopoverAnimated:YES];
}
}
return viewController;
}
#end
Now where ever you present the modal, instead of calling [self presentViewController: ... call [self.popoverPresentingViewController presentViewController: ...]. Remember to import UIViewController+Popover.h
your question came closest to my bug, On returning from modalView the parentView will switch to orientation in which the application was opened.
Visually it appears that the modal view is rotated and then returns.
I solved it by removing the modal view altogether, and using
[self.navigationController pushViewController: <the View(not modal now)>]
instead of using-
[self presentViewController:<Modal View>]
I think this is because the navigation controller doesn't own the Modal View, hence it reloads - when returning from the modal view - to incorrect orientation
Problem:
When presenting a view controller modally, it gets dismissed on rotation.
Approach:
Set the UISplitViewControllerDelegate
Use the UISplitViewControllerDelegate methods
Hold a reference to your modal view controller in an instance variable
Check if your modal view controller's presenting view controller exists.
If it exists, nothing needs to be done, else just present without any animation.
UISplitViewControllerDelegate methods:
func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
Note:
UISplitViewControllerDelegate has quite a methods, it can be daunting initially, if you spend some time experimenting, you can achieve what you want.
It has fine grained access.
I'm very late but try this. It works for me.
[self.splitViewController presentViewController:editView animated:YES completion:nil];
I am having two view controllers 'FirstViewController' and 'SecondViewController'. From first view controller it will take input from a text field ,process it and display accordingly in the second view. But I am having a problem while setting label value directly.
#interface SecondViewController : UIViewController
{
NSString *numPlate;
IBOutlet UILabel *output;
};
#property(strong,nonatomic) NSString *numPlate;
#property(strong,nonatomic) IBOutlet UILabel *output;
#end
The main file for FirstViewController.m with prepare for segue is as
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:#"Change"])
{
SecondViewController *svc = (SecondViewController *)[segue destinationViewController];
svc.numPlate = input.text;
NumberPlate *numPlate=[[NumberPlate alloc]init];
[numPlate setPlate:input.text];
NSInteger flag=[numPlate checkValidity];
if(flag==0)
{
svc.output.text =#"Invalid License";
}
else
if([numPlate getArea]==NULL||[numPlate getRegOffice]==NULL)
{
svc.output.text =#"Data not found";
}
else
{
svc.output.text =#"VALID License";
}
}
}
But when the action is performed its not working.The label is not changing.
When i used svc.numPlate instead of svc.output.text and in the SecondViewController viewDidLoad method and i used
- (void)viewDidLoad
{
[super viewDidLoad];
output.text=numPlate;
}
Everything is fine with this. Whats wrong in first method??
You will not be able to assign value directly to UILabel of second VC as view is not yet loaded into view hierarchy at this point.
So view cannot render the value assigned prior to it.
On the other hand, holding value in NSString and assigning same on viewDidLoad is working as now your view is in view hierarchy and loaded into memory.
At the time when you push SecondViewController, the SecondViewController's view hasn't been loaded yet, so you can't access its views. You need to create NSString properties in SecondViewController and pass a string to SecondViewController' NSString Object. Then in SecondViewController's viewDidLoad method, use those properties to populate the labels (which will have been loaded by the time viewDidLoad runs).
The initialisation of controller is different and presentation of its view is different process even if the viewController has been initialise its view will not because pushing the controller has not performed and controller dont know he need to load the view.. so we pass the data to controller and it load when view appears... like below code
#interface SecondViewController : UIViewController{
NSString *strMessage;
}
#property(nonatomic,retain) NSString *strMessage;
#end
SecondViewController.m
#synthesize strMessage;
- (void)viewDidLoad {
Nslog(#"%#",strMessage);
}
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([[segue identifier] isEqualToString:#"Change"])
{
SecondViewController *svc = (SecondViewController *)[segue destinationViewController];
NSString *message = [NSString stringWithFormat:#"Check out %#", nameLb.text];
svc.strMessage=message;
}
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