I am building a universal App which is supposed to present a view controller to add stuff. On iPad, this is presented in a popover and on iPhone in a modal viewController. I am using the presenting ViewController as a delegate.
Now, if I want to assign the properController, I have to:
if([[segue identifier] isEqualToString:#"popoverAddSegue"])
self.myPopOver = [(UIStoryboardPopoverSegue * )segue popoverController];
if([[segue identifier] isEqualToString:#"modalAddSegue"])
self.myModalView = [segue destinationViewController];
thus using two properties and only ever assigning one.
Then, in my delegate function, I again have to differentiate:
if(self.myPopOver)
[self.myPopOver dismissPopoverAnimated:YES];
if(self.myModalView)
[self.myModalView dismissViewControllerAnimated:YES completion:nil];
The problem obviously is that UIPopoverController does not inherit from UIViewController... Is there any elegant way of doing this? Factory or something? I hate having customised code in a viewController that should be agnostic about how it presents its viewControllers...
I use this
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
// The device is an iPhone or iPod touch.
} else {
// The device is an iPad => show library true popover.
}
This is not exactly what you ask for, but it have a good point : you can have the same segue identifier for both iPhone and iPad.
Now to have 1 similar method for both (popover / modalVC), may be you can add to both a new category with a method name dismissAnimated:
Related
Is there way to make show segue works only on iPhone? Some kind off turn it off at iPad? I have show segue on iPhone and it works perfect but on iPad I am using UISplitVC with same VC. And now every time when I choose something on left part of UISplitVC it make that segue instead my left UITableVC.
Set an identifier for the segue in Interface builder, then in your view controller class you can use this in a method instead of presenting it in your storyboard. You can do that by clicking the class button in your view controller and entering it in "Storyboard ID" as shown here.
IF YOU HAVE A BUTTON THAT YOU CLICK to present the new view you can drag an IBAction from the storyboard to this method on the view controller you are presenting it from.
-(IBAction)launchIphoneOnlyController (id)sender {
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"identifier"];
[self presentViewController:vc animated:true completion:nil];
}
}
Otherwise make a call to the following method from wherever you want to instantiate the view from.
-(void)launchIphoneOnlyController {
if ( UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ) {
UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"identifier"];
[self presentViewController:vc animated:true completion:nil];
}
}
In shouldPerformSegueWithIdentifier check the seque ID you set in the story board and check (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) for iPhone.
See apple developer.
Update for iOS 9 beta: Apple may have fixed this for iOS 9. If you work(ed) around this issue for iOS 8, make sure it also works correctly on iOS 9.
In storyboard, I've created a popover presentation segue to present a navigation and view controller from a button, as well as creating an unwind segue.
In portrait orientation, the modal (fullscreen) presentation is unwound/dismissed, as expected.
In landscape orientation, the unwind segue also gets called, however the popover presentation is not automatically dismissed.
Did I miss hooking something up? Do I have to dismiss the popover presentation myself?
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)__unused sender
{
if ([[segue identifier] isEqualToString:#"showSelectBookChapter"])
{
UINavigationController *navigationController = segue.destinationViewController;
if ([navigationController.topViewController isKindOfClass:[BIBLESelectViewController class]])
{
BIBLESelectViewController *selectViewController = (BIBLESelectViewController *)navigationController.topViewController;
selectViewController.initialBookChapterVerse = self.bookChapterVerse;
}
}
}
- (IBAction)unwindToBIBLEChapterViewController:(UIStoryboardSegue *)segue
{
if ([segue.identifier isEqualToString:#"unwindToBIBLEChapterViewController"]) {
if ([segue.sourceViewController isKindOfClass:[BIBLESelectViewController class]])
{
BIBLESelectViewController *sourceViewController = (BIBLESelectViewController *)segue.sourceViewController;
self.bookChapterVerse = sourceViewController.selectedBookChapterVerse;
[self.tableView reloadData];
}
}
}
Update:
After looking at gabbler's sample code, I've narrowed the problem down to popover dismissing fine in a Single View Application, but not in a Master-Detail Application.
Update 2:
Here's what the hierarchy looks like (omitting navigation controllers for simplicity's sake), in answer to the question Luis asked:
Split view controller
Master view controller
Detail view controller
Chapter view controller (modal page sheet)
Select view controller (the problematic popover that unwinds to chapter view controller, but doesn't dismiss)
As I mentioned in the previous update, I created an new master/detail template, and simply presented a popover directly from (a button in) the detail view. It won't dismiss.
I ran into this problem too. I present a View Controller modally (as a form sheet), from the Master View Controller (UISplitViewController). The problem only occurred on the iPad (probably the iPhone 6+ in landscape mode too, but I didn't check it). I ended up doing the following in my unwind action method (using Swift), and it works good.
if !segue.sourceViewController.isBeingDismissed() {
segue.sourceViewController.dismissViewControllerAnimated(true, completion: nil)
}
If you segue as a popover from a view controller embedded in a navigation controller, the corresponding unwind fails to dismiss the popover.
It's a bug in -[UINavigationController segueForUnwindingToViewController:fromViewController:identifier]. The embedding navigation controller is supposed to supply a segue that will dismiss the popover but it doesn't. The fix then is to override this and supply a working segue, which we can get from the embedded view controller.
Here's a partial solution that will only handle unwinding to the top view controller of the navigation stack:
#implementation MyNavigationController
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController
fromViewController:(UIViewController *)fromViewController
identifier:(NSString *)identifier
{
if (toViewController == self.topViewController && fromViewController.presentingViewController == self)
return [toViewController segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
else
return [super segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
}
#end
It works on iOS 8 for both landscape/portrait iPad and landscape/portrait iPhone. The logic should be robust enough to survive on iOS 9.
It is/must be a behavior of the popOver segue, in normal situations or regularly we need that the popOver keeps in view, if the segue show something important is annoying that we lost that information just because we rotate the device, I guess that that is the reason of that native behavior. So if we want for it to dismiss automaticly we have to make that behaivor by our own, this works:
in the method - (void)viewDidLoadin the detailViewController.m add this:
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter]
addObserver:self selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:[UIDevice currentDevice]];
then create this method:
- (void) orientationChanged:(NSNotification *)note{
UIDevice * device = note.object;
//CGRect rect = [[self view] frame];
switch(device.orientation)
{
default:
[self dismissViewControllerAnimated:YES completion:nil];
break; }}
You said that in a single view happens what you want, but I've never seen that behavior when I used popOvers.
mbeaty's fix is great but as others have pointed out, this bug seems to know be fixed in iOS 9 and it also doesn't work well for universal device design. I have adapted his answer to handle both situations. Here is the code:
#IBAction func yourUnwindSegue(segue: UIStoryboardSegue) {
if #available(iOS 9, *) {
return // bug fixed in iOS 9, just return and let it work correctly
}
// do the fix for iOS 8 bug
// access your SplitViewController somehow, this is one example
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let splitVC = appDelegate.window!.rootViewController as! YourSplitViewController
// if the source isn't being dismissed and the splitView isn't
// collapsed (ie both windows are showing), do the hack to
// force it to dismiss
if !segue.sourceViewController.isBeingDismissed() && splitVC.collapsed == false {
segue.sourceViewController.dismissViewControllerAnimated(true, completion: nil)
}
}
This first checks if iOS 9 is running and just exit as the bug seems to be fixed. This will prevent the multiple views getting dismissed issue. Also to make sure this fix is only done when the splitView is showing two windows (to make it happen only on iPads and iPhone 6 Plus in landscape as well as future devices) I added the check to make sure it is not collapsed.
I have not exhaustively check this but it seems to work. Also not that my app is set for a min of iOS 7, I don't know if this bug existed then so you may need to look into that if you support below iOS 8.
I have a simple app with 2 screens.
When I press a button to go from the first to the second, everything is performed successfully (including animation). However, when I click the back button on the second screen, I get the following warning:
Warning: Attempt to present <getTextViewController: 0x8f6aa30> on <SecondViewController: 0x946cc80> whose view is not in the window hierarchy!
EDIT: Please don't refer me to other questions regarding above warning - I already saw those, and they refer to other issues.
However, it still switches back to the first screen. Yet, the animation of the segue does not perform.
Also: Information (such as inputted text) in the first screen remains when I return to the first screen, while information in the second screen resets every time the screen comes up.
Here is how I call both operations:
Segue from View 1 to View 2:
Name: F21, Style: Modal, Transition: Cross Dissolve, Animation: True.
Segue from View 2 to View 1:
Name: F12, Style: Modal, Transition: Cross Dissolve, Animation: True.
Code in getTextViewController.m (View 1):
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"F21"]){
UIViewController *v = [segue destinationViewController];
[self dismissViewControllerAnimated:NO completion:nil];
v = self;
}
}
-(void)performSegue:(NSString*)str{
[self performSegueWithIdentifier:str sender:self];
}
//In some other method:
[self performSegue:#"F21"];
Code in SecondViewController.m (View 2):
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
if([[segue identifier] isEqualToString:#"F12"]){
UIViewController *v = [segue destinationViewController];
[self dismissViewControllerAnimated:NO completion:nil];
v = self;
}
}
-(void)performSegue:(NSString*)str{
[self performSegueWithIdentifier:str sender:self];
}
- (IBAction)goBack:(id)sender {
[self performSegue:#"F12"];
}
I would very much appreciate any help to understand why the first segue works while the second doesn't.
Thank you,
Dean
NOTE: Here is the full project - https://github.com/dean13-meet/firstIOSApp
EDIT: Updated git.
Im not exactly sure what you're trying to do in your prepareForSegue, their is no need to be dismissing VC's there. If you want to have a simple app where you go from VC1 to VC2 and then back again, your best bet is to use a segue and an unwindSegue.
So in your storyboard control drag from a button on VC1 to VC2 and select your segue type. Then in VC1.m setup the unwind segue such as:
- (IBAction)unwindFromViewController:(UIStoryboardSegue *)segue
{
//empty implementation
}
Finally, in your VC2 control drag from the back button to the green exit icon on VC2 and select your unwindFromViewController method.
That should do what you're looking for.
For the sake of simplicity, I would suggest using a push segue opposed to modal because it takes care of all the back buttons for you. If you don't like the idea of a navigation controller however, try dismissing the view with the following: Moving back from a Controller to a previous one
I have this problem:
I have two ViewControllers.
I am transitioning to the second view with Segue.
User enters his name on first view controller and taps on a button. If the text is nil, it should not show the second view controller. If some text is there on the text field - it has to show the next view controller.
I am checking the text length here below. As I have only one segue... I am not checking segue identifier.
And, on Storyboard, I have give modal transition CoverVertical. The animation is not working. View is just appearing. UIModalTransitionStyle also I tried. Still not working (on device and simulator)
-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender
{
if(playerNameTextField.text.length == 0)
{
UIColor *tempColor=UIColorFromRGB(0xFF4981);
[self colorizeTextViewForAWhile:playerNameTextField withUIColor:tempColor animated:YES];
return NO;
}
return YES;
}
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([segue.identifier isEqualToString:#"segueMoveToHome"])
{
ViewController *vc = (ViewController *)segue.destinationViewController;
vc.playerName=playerNameTextField.text;
}
}
Your code works perfectly. I just reproduced and I've got a transition with CoverVertical.
In Interface Builder, select the Storyboard Segue and be sure that you have:
I'm linking you a mini demo with your code that works.
I have removed the Identifier text and continued. As it was the only segue or any other reason... it was working. If I put the segue... it is not working. :| Not sure. But I have now my app working.
First, a little background. I'm new to iOS development, I've been in .Net land for a long time, and that's probably why I'm even asking this question, but here goes.
The basic setup is this. You have a UINavigationController with a RootViewController we'll call MasterViewController. When some action happens on this MasterViewController, we want to drill into a DetailsViewController. However, we also want to pass some data to the DetailsViewController.
It is my understanding, that in previous versions of the SDK (prior to iOS 5) the approach was similiar to this:
#implementation MasterViewController
-(IBAction)someAction
{
DetailsViewController *dvc = [[DetailsViewController alloc]initWithNibName:#"DetailsView" bundle:nil];
dvc.someDataProp = [self getSomeDataSomeHow];
[[self navigationController] pushViewController:dvc animated:YES];
}
#end
Now however, in iOS 5, it seems that this is now done using the Storyboard and segues. In XCode you set up the segue from the MasterViewController to the DetailsViewController, and then in code you do something like this:
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
[segue.destinationViewController setSomeDataProp:[self getSomeDataSomeHow]];
}
My question is basically this: The older approach somehow feels a lot cleaner to me. You're being very explicit about the type of ViewController you're pushing on to the navigation stack and you can set properties easily on it. In the new approach though, destinationViewController is of type id (for obvious reasons), and it just feels a lot less clean to me. Again, this could be my .Net side coming out, but is this common in iOS? Just use id and throw caution to the wind?
With Storyboards you can assign a named identifier to the segue,
Select the segue and in the Attribute inspector you can add a name to the segue Identifier.
And in the prepareForSegue method you should check for this Identifier and thus you will explicitly know which segue is about to be performed and what the destinationViewController will be.
if ([segue.identifier isEqualToString:#"My First Segue Identifier"])
{
DetailsViewController *dvc = (DetailsViewController *) segue.destinationViewController;
// Set the DVC's properties
}
In many cases, the destination view controller for a segue may be a UINavigationViewController, and in that case, the solution (a slight modification of Dennis Mathews' solution above) will need to use the message"topViewController":
if ([segue.identifier isEqualToString:#"My First Segue Identifier"])
{
NavitationViewController* navController = [segue destinationViewController];
DetailsViewController *dvc = (DetailsViewController*) [navController topViewController]
// Set the DVC's properties
}
I haven't been working with iOS that long, but I've seen a few examples where you don't have to cast because of objective-c's loose coupled messaging system.
Instead of checking the segue identifier or casting to a specific ViewController you can do this:
-(void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.destinationViewController respondsToSelector: #selector(setCompany:)]) {
[segue.destinationViewController performSelector: #selector(setCompany:) withObject: self.company];
}
}
In the first line I ask if the destinationViewController has a method setCompany (if you have a property named company this one would be generated for you). If it does, you can call that method/set that property with the second line of code.
So in this case you don't really have to know the destination ViewController and could easily replace it with a different one that supports handling Companies.