I am facing a strange iOS 8.3 issue which shows a keyboard on a wrong orientation like this (the view controller is in Landscape mode, but the keyboard show up in Portrait mode):
I can trigger this issue by following these steps:
Create 2 UIViewController subClass: ViewControllerA and ViewControllerB
in ViewControllerA implement supportedInterfaceOrientations and return UIInterfaceOrientationMaskPortrait
in ViewControllerB implement supportedInterfaceOrientations and return UIInterfaceOrientationMaskLandscape
Create a UINavigationController subclass called NavigationController, implement supportedInterfaceOrientations and return [self.topViewController supportedInterfaceOrientations] (I'm doing this because I want to keep the NavigationController and it's rootVC from rotating)
Use the NavigationController as initial view controller of the app, set ViewControllerA as the NavigationController's rootViewContrller
Launch the app, ViewControllerA will shown up in Portrait. Show a button on ViewControllerA, press the button will present ViewControllerB by using presentViewController:animated:completion
ViewControllerB will show up in Landscape; Show a text field on ViewControllerB, tap on the text field will trigger the keyboard, but the keyboard is in Portrait mode, just like the image above.
PS. You can download and run the Xcode project on github
This issue seems only appears on iOS 8.3. Am I doing something wrong ? Or maybe this is just another bug of iOS ?
By the way, this issue won't happen if you just show ViewControllerA directly without a ViewController. So if this is a bug of iOS, how can I avoid subclassing UINavigationController but still keep ViewControllerA which is the rootViewController of a UINavigationController from rotating.
UPDATE: This bug still appears on iOS 8.4, I fired a bug report and got replies from apple on June 17th 2015, they said it has been addressed in the latest iOS 9 beta.
I've asked a tech support and they said
Our engineers have reviewed your request and have determined that this would be best handled as a bug report.
So seems that it's confirmed a system bug.
And my current solution is adding a ViewControllerC (support both portrait and landscape mode) between ViewControllerA and ViewControllerB: ViewControllerA should present ViewControllerC with animation, after ViewControllerC appeared, present ViewControllerB without animation.
To make the transition looks natural, you can set the same background colour as ViewControllerB to ViewControllerC, or take a screen shot of ViewControllerB and add it to ViewControllerC as a background image.
It's not a perfect solution, for users, they may find out ViewControllerB takes more time to load.
We got around this issue by tricking the device into thinking that it was rotated into a different orientation and then into its intended orientation.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.segmentedControl.selectedSegmentIndex = 0;
if(!self.rotatingForDismissal)
{
[self.tableNumberTextField becomeFirstResponder];
}
if(SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.3"))
{
[[UIDevice currentDevice] setValue:#(UIDeviceOrientationLandscapeLeft) forKey:#"orientation"];
[[UIDevice currentDevice] setValue:#(self.interfaceOrientation) forKey:#"orientation"];
}
}
I was able to make the keyboard appear correctly using the following.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (SYSTEM_VERSION_GREATER_THAN(#"7.1") && SYSTEM_VERSION_LESS_THAN(#"9.0"))
{
UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
[[UIDevice currentDevice] setValue:#(UIDeviceOrientationUnknown) forKey:#"orientation"];
[[UIDevice currentDevice] setValue:#(orientation) forKey:#"orientation"];
}
}
Note that using UIDeviceOrientationUnknown does not cause side effects such as inappropriate auto rotation of views.
Related
I can change the rotation through several tricks (e.g calling [[UIDevice currentDevice] setValue:rotation forKey:#"orientation"];, or presenting a UIViewController with the wanted rotation methods overriden, and dismissing it shortly after).
The issue is, when I disable the rotation lock on the phone, and rotate the phone to the wanted rotation, in a UIViewController where it isn't supported, but than move to a UIViewController (via a pop) where this rotation is supported, the rotation is not applied correctly.
Any ideas how to solve this issue?
I think I understand what you're asking and I've faced this problem before. I solved with putting this in the view controller that's getting popped:
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if ([[UIDevice currentDevice] respondsToSelector:#selector(setOrientation:)]) {
[[UIDevice currentDevice]performSelector:#selector(setOrientation:) withObject:(__bridge id)((void *)UIInterfaceOrientationPortrait)];
}
}
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 been working an iOS 7 app to make it compatible for ios 8 (beta 5). In this application, UIViewController (vc1) presents another UIViewController (vc2). vc1 supports both Portrait and Landscape orientations; vc2 supports only Portrait orientation. When vc2 is presented, it asks vc1: shouldAutorotateToInterfaceOrientation: and this returns YES.
In iOS8 (Beta 5) willRotateToInterfaceOrientation: and didRotateFromInterfaceOrientation: are not getting called as well as the new iOS 8 API method viewWillTransitionToSize. But, this works fine in iOS7.
I know willAnimateRotationToInterfaceOrientation and didRotateFromInterfaceOrientation are deprecated in iOS 8, but even iOS 8 delegate methods are not getting called. Every time when launched vc2 from vc1 always screens loads in portrait mode only even though I mentioned supported interface orientation as landscape left.
Any ideas... is it a bug in iOS8?
Well, I didn't figure out your problem best but as soon I have a bunch of lines working fine with rotation in iOS 8.1 I will present them to you. They are just taken and a little bit of edited from the Apple API Reference.
Simply I put this in every VC and i just edit the code when needed. For example I have an app that have initial view controller in portrait and then VC changes ( segue is done ) to a LandscapeVC with different features.
This is the portrait view methods leading to a rotation in LandscapeView.
bool isShowingLandscapeView = false;
- (void)awakeFromNib
{
isShowingLandscapeView = NO;
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
}
- (void)orientationChanged:(NSNotification *)notification
{
UIDeviceOrientation deviceOrientation = [UIDevice currentDevice].orientation;
if (UIDeviceOrientationIsLandscape(deviceOrientation) &&
!isShowingLandscapeView)
{
isShowingLandscapeView = YES;
[self performSegueWithIdentifier:#"toLandscape" sender:self];
}
I hope I made it simple for understanding. Don't hesitate to improve my answer, we all learn in this life !
I have an old app, that still lives on iTunes, written in iOS 5. I would like to update it to run on ios 6 and 7. Everything has been fine so far, and I have updated my code to use ARC. However when trying to maintain the same autorotation philosophy I keep hitting a brick wall. I have already checked relative topics within SO like:
Forcing landscape and autorotate in iOS 7,
Autorotate in iOS 6 has strange behaviour
and following a similar topic I have found this:
iOS 6 Autorotation Problems and Help
which lead me to do the following:
I have set the rootViewController within my AppDelegate like so:
self.preloadingViewController = [[PreloadingViewController alloc] initWithNibName:#"PreloadingViewController" bundle:nil];
self.window.rootViewController = self.preloadingViewController;
I have placed:
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
within my AppDelegate. I have overriden shouldAutorotate and supportedInterfaceOrientations within the SuperViewController (parent in inheritance terms) of all of my app's UIViewControllers (including PreloadingViewController mentioned above):
- (BOOL)shouldAutorotate{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
and in every child UIViewController, I override
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
with code to layout ui elements in the desirable manner for portrait and landscape orientations.
Finally my app's plist file under
Supported interface orientations
contains:
Portrait (bottom home button), Landscape (left home button), Landscape
(right home button)
all the orientations I want to support.
Still, even though supportedInterfaceOrientations and shouldAutorotate are being called for every orientation change on my rootViewController, willAnimateRotationToInterfaceOrientation is never being called. I have even overriden shouldAutomaticallyForwardRotationMethods in my SuperViewController to return YES, but to no avail.
What am I doing wrong here? Any ideas? I have even considered that the old ios5 - style xibs cause the issue but I do not think this is the case.
Thanks in advance.
In iOS 6 the way willAnimateRotationToInterfaceOrientation is called changed.
Use:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
// Something
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// Something
}
in your rootController.
EDIT:
New main.m
#import <UIKit/UIKit.h>
#import "yourAppDelegate.h"
int main(int argc, char *argv[])
{
#autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([yourAppDelegate class]));
}
}
Since my rootViewController was the only one getting its shouldAutorotate and supportedInterfaceOrientations methods called, I decided to register every other view controller to get notified of any UIDeviceOrientationDidChangeNotification.
[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
- (void)orientationChanged:(NSNotification *)notification
{
NSLog(#"Orientation changed!");
[self layoutForOrientation:[[UIDevice currentDevice] orientation]];
}
Within my layoutForOrientation: method I handled the uiview's controls orientation.
However, although I did receive UIDeviceOrientationDidChangeNotification notifications normally, my view orientation would not actually change to match the current orientation, i.e. if the new orientation was say, UIInterfaceOrientationLandscapeRight, the view orientation would remain in UIInterfaceOrientationPortrait and its children views would get rotated by 90 degrees. This did certainly not look right. After pulling a lot of hair out, I decided to abandon this route.
Instead, I have set my rootViewController to be a UINavigationController and have it push successive UIViewControllers on top of it. Since I did not want a navigation bar visible in my app I have set the navigation controller's navigationBarHidden property set to YES. This way the method willAnimateRotationToInterfaceOrientation is getting called on every UIViewController that is currently at the top of the navigationCotroller's stack of controllers, and its corresponding view and view controls rotate correctly for the desired orientations.
This works because most of my view controllers supported interface orientations match the mask: UIInterfaceOrientationMaskAllButUpsideDown, the default behaviour for iPhone. Should I needed to support different orientations, I would have to subclass the UINavigationController and override supportedInterfaceOrientations and shouldAutorotate to enable the desired orientations support for the navigation stack's top view controller, as per Apple's example project: AlternateViews.
Implement this method, return YES for what you need.
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
If you have an app with one UIWindow, my understanding is that the rootViewController of that UIWindow will be the UIViewController that receives the rotation/orientation methods like shouldAutoRotate, shouldAutoRotateToInterfaceOrientation, etc.
I'm writing an external library and there is an instance where I create another UIWindow object, set its rootViewController, and make it key and visible. It seems like the rootViewController of the original window is the one that still gets sent the rotation methods and not the new one.
I want to be able to control whether the application can rotate or not while the new window is visible, but it seems like the original window's rootViewController still has control over that. I've tried setting the original window's rootViewController to a rootViewController that prohibits rotation of the screen while my new window is visible and resetting the original window's rootViewController to its original rootViewController but that causes some problems of its own.
Does anyone know how to make a certain UIViewController the one in charge of app rotation?
How are you presenting the new viewController? According to the documentation,the only viewControllers that are asked about supportedInterfaceOrientations are the root view controller, or a view controller that fills the screen. So, on an iPhone, your new viewController should receive the supportedInterfaceOrientations call if it is filling the screen (e.g., presented modally).
shouldAutoRotateToInterfaceOrientation was deprecated as of iOS 6, so you should override supportedInterfaceOrientations instead.
This has worked for me...
In my Case, the destination view appears correct but the status bar and also the UIKeyboard keeps the landscape configuration, making a real mess.
Working around After thousands of recommendations about statusBarOrientation and references read... https://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/_index.html
"The setStatusBarOrientation:animated: method is not deprecated outright. It now works only if the supportedInterfaceOrientations method of the top-most full-screen view controller returns 0. This makes the caller responsible for ensuring that the status bar orientation is consistent."
statusBarOrientation only works if supportedInterfaceOrientations returns 0, so... that give us a guess.
If statusBarOrientation is not as expected, one zero return will do it (if always return 0, the view wont rotate, so:
// if deviceOrientation is A (so I expect statusbarOrientation A
// but statusbarOrientation is B
// return 0
// otherwise
// return user interface orientation for A
- (NSUInteger)supportedInterfaceOrientations {
UIDeviceOrientation deviceOrientation = [[UIDevice currentDevice] orientation];
UIInterfaceOrientation statusBarOrientation =[UIApplication sharedApplication].statusBarOrientation;
if(deviceOrientation == UIDeviceOrientationPortrait || deviceOrientation == UIDeviceOrientationPortraitUpsideDown){
if(statusBarOrientation != UIInterfaceOrientationPortrait ||statusBarOrientation != UIInterfaceOrientationPortraitUpsideDown){
return 0;
}
}
// otherwise
return UIInterfaceOrientationMaskPortrait;
}
Now, in viewDidAppear (believe me, I use this call even when the keyboard notification is recived:
[UIApplication sharedApplication].statusBarOrientation = UIInterfaceOrientationPortrait;