This is my first post on SO, but I am very familiar with the site. I currently have an app that was working correctly until a recent iOS update. Here is the situation:
I have a PageViewController with a page that holds a TableViewController. When the phone orientation is changed from portrait to landscape, a modal view is created and presented to the user, using a modal segue. If the user then changes the orientation of the phone back to portrait, the app crashes.
I have spent many long hours trying to do work arounds and find/create solutions, but no luck. What I have discovered is "dismissViewControllerAnimated" is being called and the modal view is being deallocated. However, I have a print statement in at the beginning of my "orientationChanged" function. It prints once after the first time from landscape to portrait. But if the user rotates the phone to landscape again and back to portrait, "orientationChanged" will now be called twice, as though the view controller was never removed from the stack. If the user proceeds to do this 10 times, for example, on the 10th time "orientationChanged" will be called 10 times in a row.
This just doesn't make any sense to me. If the modal view is being deallocated, then how is this possible? Is there a copy of the modal view being stored somewhere? I tried to change the modal segue to be a push segue, but no luck. It would result in the same thing. Using the push segue and navigation bar that comes with it, I was able to navigate backwards through the view stack. The stack had duplicate copies of my desired modal view, like they were never removed from the stack, and at the bottom of the stack was my TableViewController.
Here is my orientationChanged function:
- (void)orientationChanged:(NSNotification *)notification
{
NSLog(popModalView ? #"Yes" : #"No");
NSLog(#"Index:%lu",(unsigned long)self.pageIndex);
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
if (UIDeviceOrientationIsLandscape(deviceOrientation) && popModalView == NO)
{
[self performSegueWithIdentifier:#"DisplayGraphNumberOfJobs" sender:self];
popModalView = YES;
}
else if (UIDeviceOrientationIsPortrait(deviceOrientation) && popModalView == YES)
{
NSLog(#"dismiss");
[self dismissViewControllerAnimated:YES completion:nil];
popModalView = NO;
}
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"DisplayGraphNumberOfJobs"])
{
GraphJobTotalsVC *graphJobs = [segue destinationViewController];
//initialize graphJobs...
}
}
As a side note, my PageViewController has 3 pages, each with the same problem as this one. So solving one page should solve them all, hopefully. I have looked all over apple's documentation, but no luck. I have looked at countless other SO answers as well. I hope someone can help me. Thank you, in advance.
Related
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 UIViewController then with this code I present a modal view controller for entering the password.
[self performSegueWithIdentifier:#"SeguePassword" sender:self];
in Storyboard:
Storyboard Segue
Identifier = SeguePassword
Style = Modal
Transition = default
Animates = not checked
When I click "Cancel" on the modal which has the following code:
[self dismissViewControllerAnimated:NO completion:^{}];
Now when I get back the keyboard is hidden. I have code to show a toolbar with a button called hide. And I see that, but not the keyboard.
Does anyone have any ideas or directives they have taken to fix an issue like this? It seems it recently started after converting changes for iOS 7.
My solution:
I found this because after I clicked the but on an alert box a few times out of frustration I saw the keyboard in the wrong orientation. I.e.., a landscape keyboard when the app is portrait only on iPhone.
Before:
- (NSUInteger) supportedInterfaceOrientations {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationLandscapeLeft | UIInterfaceOrientationLandscapeRight;
} else {
**return (UIInterfaceOrientationPortrait);**
}
}
After:
- (NSUInteger) supportedInterfaceOrientations {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationLandscapeLeft | UIInterfaceOrientationLandscapeRight;
} else {
**return (UIInterfaceOrientationMaskPortrait);**
}
}
I changed the Portrait to use the Mask integer and everything started working again.
The keyboard is only visible if some object is the firstResponder. Look at the documentation for the UIResponder class.
If your modal view controller uses the keyboard, which it sounds like it does, it takes first responder status and whatever had it on your original screen loses it. If you want to return to the same state from the modal view controller, with some object having input focus -- i.e. being the firstResponder -- with the keyboard visible you will have to have write code to make that happen. In particular, your original view controller has to know when the modal view controller terminates so that it can make your input object, textField or textView or whatever, be the first responder with the statement:
[objectIWantToBeFirstResponder becomeFirstResponder];
Now how do you know when the modal view controller is done?
There are a couple obvious techniques.
If you want to use a segue then the modal view controller will have to
explicitly let the presenting view controller know that it is
terminating. You will probably want to define a protocol in the
modal view controller and have the presenting view controller adopt
it. When the modal view controller wants to exit it first tells the presenting
view controller.
Since you are manually triggering the segue perhaps
you're willing to present the modal view controller in code. If so then
you can use a completion block:
ModalViewControllerClass *vc = [[ModalViewControllerClass alloc] init];
// whatever else you need to do to the vc
[self presentViewController:vc animated:YESorNO completion:^{
// This code gets executed when the presented view controller exits
[objectIWantToBeFirstResponder becomeFirstResponder];
}];
I have a view that requires user to be logged in. When the user attempts to open that view an he is not logged in I will call the login view for him to login and after he is done I will call the original view that he intended to see.
On iPhone this works fine as I push view controllers there.
But on iPad where I present view controller this does not work. It says that dismissal in progress, can't show new controller. Here is the code:
- (void) buttonPressed
{
if (!userLoggedIn) { // userLoggedIn getter calls new screens of login if needed
return; // this is executed if user declined to login
}
MyViewController *temp = [[MyViewController alloc] init];
[self.navigationController presentViewController:temp animated:YES]; // this returns warning that dismissal in progress and does not work
}
What can I do about that? On iPhone all of my logic works fine, but on iPad it fails. I use it in many places and completely rewriting code is not good.
EDIT: more code:
- (BOOL) userLoggedIn {
// code omitted
[centerController presentViewController:navController animated:YES completion:nil];
// code omitted
[centerController dismissViewController:navController animated:YES]; // setting to NO does not fix my problem
return YES;
}
EDIT2:
This is the code for iPad. I have removed iPhone-related code. What it does on iPhone - instead of presenting controller it uses pushing, and in that situation everything works fine.
You cannot present another view as long as the dismissing of your 1st view is not complete. The animation of dismissing view should be completed before presenting new view. So, either you can set its animation to NO while dismissing, or use
performSelector:withObject:afterDelay:
and present the next view after 2-3 seconds.
Hope this helps.
You've not posted enough code to really see what you're doing, but one approach to the problem of dismissing and pushing view controllers clashing in this way is to make a the pop+posh into a single atomic operation operation, rather then seqential operations.
You can do this by using the setViewControllers:animated: method on UINavigationController. This allows you to effectively remove one or more view controllers, and add one or more view controllers, all as one cohesive operation, with one seamless animation.
Here's a simple example:
[self.navigationController pushViewController:loginController];
// ... later on, when user login is validated:
NSMutableArray *viewControllers =
[self.navigationController.viewControllers copy];
[viewControllers removeLastObject];
[viewControllers addObject:[[MyNewViewController alloc] init]];
[self.navigationController setViewControllers:viewControllers animated:YES];
If you do things this way, your code will be more predictable, and will work across iPhone and iPad.
For more info, see the API docs.
Update
Since your problem involves a modal dialog on top, try using setViewControllers:animated:NO to change the nav controller stack underneath the modal login dialog before you dismiss the modal.
I am trying to implement a hide/unhide feature for the master view controller of my UISplitViewController. So the master view controller should be present in portrait and landscape mode but just for a specific view (the settings). Everywhere else it should appear in landscape only.
In -(void)viewDidAppear:(BOOL)animated of my MasterController I am writing
self.popoverController.delegate = self;
appDelegate.splitViewController.delegate= nil;
appDelegate.splitViewController.delegate = self;
[appDelegate.splitViewController willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[appDelegate.splitViewController didRotateFromInterfaceOrientation:self.interfaceOrientation];
appDelegate.splitViewController.view setNeedsLayout];
The delegate method is also set
- (BOOL)splitViewController:(UISplitViewController *)svc shouldHideViewController:(UIViewController *)vc inOrientation:(UIInterfaceOrientation)orientation
{
return NO;
}
This approach I found here on stackoverflow really works well, but just as long as I rotate the device. Then the master view controller disappears (and leaves a black space). While rotating it appears and as soon as the rotation is done it disappears again. Further the master view controller disappears complelty if I click outside.
So I implemented the following delegate method to prevent the popover from disappearing
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
return NO;
}
That works too, but then the tableView of my detail view is not responding.
If I delete the code from -(void)viewDidAppear:(BOOL)animated and the UIPopoverControllerDelegate method, it works all as expected, but just after I rotate the device.
So my question is, if anyone has an idea how I can solve that problem. The solution should be able to work with iOS 5.0 and newer.
Thanks a lot for your answers!
I haven't found anything corresponding to my situation so far...
FYI, I'm developing for iOS 5, using a storyboard.
I have a tab bar controller with 2 views in it (let's call them tab 1 and tab 2). I also have a separate landscape view, with no tab bar, which is used any time the device rotates during the application use. I use a segue launched manually in shouldAutorotateToInterfaceOrientation to switch to and from this view. I also use a NSString in the landscape view to know which tab I am coming from, to go back to correct one when I go back to portrait. So far, this works fine. I can go to and from landscape mode exactly the way I want.
My problem is :
When I launch the app, in portrait, I see the tab bar. If I go to landscape, it disappears. This is fine, that's what I did in my storyboard. But when I go back to portrait, the tab bar does not come back ! That's the problem.
Edit : code calling the rotation
I stopped using shouldAutorotateToInterfaceOrientation to rotate because it was conflicting with the custom segues. The problem with the tab bar was here before, so this is not the issue. I use didRotate instead.
Here is the code from FirstViewController.m (same code in SecondViewController.m, changing my segue identifier) :
-(void)didRotate:(NSNotification *)notification
{
UIInterfaceOrientation newOrientation = [[UIDevice currentDevice] orientation];
if ((newOrientation == UIInterfaceOrientationLandscapeLeft || newOrientation == UIInterfaceOrientationLandscapeRight))
{
[self performSegueWithIdentifier: #"Page1ToLandscapeSegue" sender: self];
}
}
And from LandscapeViewController.m (previousView is a NSString, set before going to landscape, so I know which view I'm coming from) :
-(void)didRotate:(NSNotification *)notification
{
UIInterfaceOrientation newOrientation = [[UIDevice currentDevice] orientation];
if (newOrientation == UIInterfaceOrientationPortrait)
{
if ([previousView isEqualToString: #"View1"]) {
[self performSegueWithIdentifier: #"LandscapeToPage1Segue"
sender: self];
}
else if ([previousView isEqualToString: #"View2"]) {
[self performSegueWithIdentifier: #"LandscapeToPage2Segue"
sender: self];
}
}
}
Looking at your comments I'm thinking that your tab bar is disappearing because you're seguing from a view controller that is not embedded in a tab bar controller (this being your landscape view view controller), I'd suggest the following:
1) It seems complicated to setup segues to go back to the previous view, not to mention that you're creating more views/controllers and adding them to the stack, so discard the segues that go back to your original views.
2) Make the segues to the landscape view be modal, that way you won't have the tab bar show up when you segue to them, if you use push it will be embedded in the tab bar controller.
3) Since the landscape view would be a modal view, simply call this method in your rotate code in the landscapes view controller:
[[self presentingViewController] dismissModalViewControllerAnimated:YES];
This will push the view off the stack and go back to the view it came from.