I have a navigation controller which have a few view controllers. I need to support all orientations for all view controllers except one special view controller which only supports landscape. This special view controller appears in the middle of the navigation stack. I have done quite a lot of research but couldn't find any good solution. Here are the links that I have read and tried.
http://www.iphonedevsdk.com/forum/iphone-sdk-development/3219-force-landscape-mode-one-view.html#post60435
How to rotate screen to landscape?
How to autorotate from portrait to landscape mode?
iPhone - allow landscape orientation on just one viewcontroller
http://goodliffe.blogspot.com/2009/12/iphone-forcing-uiview-to-reorientate.html
Next I am going to try to replace navigation controller with presentModalViewController in order to display the special view controller. Then I am going to create a new navigation view controller inside the special view controller to push the subsequent view controllers.
If anyone has a better idea, please let me know. Really appreciated!
UPDATE: I have successfully use the method I described above: replace pushViewController with presentModalViewController and create a new navigation controller.
Every view controller pushed onto the navigation controllers stack have to support the same orientations. This means that it is not possible to have some view controllers only supporting portrait and others only supporting landscape. In other words all view controllers on the same navigation controller stack should return the same in the delegate:
(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
But there is a simple solution to this! Here is an example for going from portrait to landscape. Here is the steps to do it and below is code to support it.
Create a ‘fake’ view controller that will be root in a sub navigation controller. This view controller should support landscape.
Create a new instance of a UINavigationController, add an instance of the ‘fake’ view controller as root and an instance of your landscape view controller as second view controller
Present the UINavigationController instance as modal from the parent view controller
First, create a new view controller (FakeRootViewController) with this code:
#interface FakeRootViewController : UIViewController
#property (strong, nonatomic) UINavigationController* parentNavigationController;
#end
#implementation FaceRootViewController
#synthesize parentNavigationController;
// viewWillAppear is called when we touch the back button on the navigation bar
(void)viewWillAppear:(BOOL)animated {
// Remove our self from modal view though the parent view controller
[parentNavigationController dismissModalViewControllerAnimated:YES];
}
(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation == UIInterfaceOrientationIsLandscape(interfaceOrientation));
}
Here is the code to present the view controller that you wish to show in landscape mode:
FakeRootViewController* fakeRootViewController = [[FakeRootViewController alloc] init];[fakeRootViewController.navigationItem setBackBarButtonItem:backButton]; // Set back button
// The parent navigation controller is the one containing the view controllers in portrait mode.
fakeRootViewController.parentNavigationController = parentNavigationController;
UINavigationController* subNavigationController = // Initialize this the same way you have initialized your parent navigation controller.
UIViewController* landscapeViewController = // Initialize the landscape view controller
[subNavigationController setViewControllers:
[NSArray arrayWithObjects:fakeRootViewController,
landscapeViewController, nil] animated:NO];
[_navigationController presentModalViewController:subNavigationController animated:YES];
Remember that the landscapeViewController should also have this implementation:
(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return (interfaceOrientation == UIInterfaceOrientationIsLandscape(interfaceOrientation));
}
There's a private API to force an orientation change. Put in your pushed view controller's -viewWillAppear::
if ([UIDevice instancesRespondToSelector:#selector(setOrientation:)]) {
[[UIDevice currentDevice] setOrientation:UIInterfaceOrientationPortrait];
}
To suppress the compiler warning, add this to the .m file of your view controller:
#interface UIDevice()
- (void)setOrientation:(UIDeviceOrientation)orientation; // private API to let POIEntryVC be pushed over landscape route view
#end
As always, there's a risk of being rejected and a risk of breaking in future OS versions when using private APIs. Do at your own risk!
Generally, presenting a modal view controller is the better solution in most cases.
You can make actions:
Change Your code with accordance of schellsan suggestion, next -
Try to add currentViewController(which will push to navigation viewController) as property to appDelegate. When You attempt to push view controller, set it to current view controller before this. Next - make a subclass of rootViewController in navigation controller. In this subclass owerload method
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
return [appDelegate.currentViewController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
}
It should works if You not using a navigation bar and pushes new controller after popping an old
It should be as simple as implementing
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
in each UIViewController pushed into your UINavigationController. In the case that one UIViewController's view shouldn't rotate, return NO for that specific orientation in that specific UIViewController.
There's a gotcha here though, if your UINavigationController implements
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
it will block its viewControllers from receiving that method. In that case, you should forward the message to the topViewController using
[[self topViewController] shouldAutorotateToInterfaceOrientation:interfaceOrientation];
You could try this code in your UINavigationController to call the current visible view's shouldAutorotateToInterfaceOrientation. In my case I have the UINavigationController in a UITabBarController but you could probably adapt it to other cases.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
if ([appDelegate.tabBarController.selectedViewController respondsToSelector:#selector(topViewController)])
{
UINavigationController *nc = (UINavigationController*)appDelegate.tabBarController.selectedViewController;
return [nc.topViewController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
}
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
Related
I have an iPhone application most of which is portrait-only, but with one view controller (a video player screen) that has to support both portrait and landscape (this is for iOS 8 only). To achieve this, I have set the app's plist to support portrait and both kinds of landscape, then subclassed UINavigationController; in this subclass I override
- (BOOL)shouldAutorotate {
return YES;
}
and
- (NSUInteger)supportedInterfaceOrientations {
if ([self.visibleViewController isKindOfClass:[MyVideoPlayerScreen class]]) {
return UIInterfaceOrientationMaskAllButUpsideDown;
} else {
return UIInterfaceOrientationMaskPortrait;
}
}
This mostly works as expected: the initial screens are all portrait-only, and remain in portrait even when the device is turned to landscape. The video player, when initially presented:
MyVideoPlayerScreen *newVC = [MyVideoPlayerScreen new];
[self.navigationController pushViewController:newVC animated:YES];
is also in portrait but will then rotate to landscape when the device is turned - all good so far.
The problem is that if I turn the device to landscape, the video player goes landscape as well, but then when I dismiss the video player screen via the back button, the underlying view controller (which is supposed to be portrait-only) is now also in landscape. If I rotate the device back to portrait the view controller rotates back to portrait as well, and it is then correctly locked in portrait-only from that point on.
How can I get the original view controller (which is supposed to be portrait-only) to automatically go back to portrait when the landscape view controller above it is popped?
This question has been asked a million times, but it seems that the fixes that were posted for it are all hacks that don't work in iOS 8 any more.
Update: I have found a "sort-of" fix for this that does work in iOS 8. In my UINavigationController subclass, I handle the <UINavigationControllerDelegate> protocol. I implemented this method:
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (![viewController isKindOfClass:[MyVideoPlayerScreen class]]) {
// if the current orientation is not already portrait, we need this hack in order to set the root back to portrait
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if (orientation != UIInterfaceOrientationPortrait) {
// HACK: setting the root view controller to nil and back again "resets" the navigation bar to the correct orientation
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
UIViewController *vc = window.rootViewController;
window.rootViewController = nil;
window.rootViewController = vc;
}
}
}
This at least leaves the UI in the state I want it to be in. The problem is that when the top-level view controller is dismissed and animated off-screen, the underlying view controller is still in landscape; then it suddenly jumps to portrait. Not ideal, but better than nothing.
Update 2: I should have added that I am pushing the second view controller like so:
ViewControllerPortraitLandscape *newVC = [ViewControllerPortraitLandscape new];
[self.navigationController pushViewController:newVC animated:YES];
Update 3: OK, this is totally doable, in a way that works for iOS 6 and up. The fix even kind of makes sense, although the reason for it's working does not seem to be in the documentation anywhere. Basically, in the view controller that you need to be reset to portrait when the top-level view controller is dismissed while the device is still in landscape, you just need to add this:
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
Although my subclass of UINavigationController is entirely responsible for the rotation calls, breakpoints show that supportedInterfaceOrientations is called on the underlying view controller just before the top-level is dismissed, and it's called on the navigation controller after the top-level is dismissed. So I'm inferring that this call to the view controller itself is made by iOS in order to determine what orientation the underlying view controller should be in (and it does not ask the nav controller for this); if it's not explicitly overridden it will return the all-but-upside-down parameter, so iOS just leaves it where it is, in landscape.
Turns out this is an easy fix: you can drive the entire process via a subclass of UINavigationController as I posted here (i.e. not implementing any of these rotation methods in the view controllers themselves), except that for any view controller that needs to be portrait-only, you also need to implement this:
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
With breakpoints, you can see that this method is called before the pushed view controller above it is dismissed; iOS presumably uses this call to determine what orientation the "revealed" view controller should be in (the same call to the navigation controller subclass is called after the top-level view controller is dismissed).
With this method implemented in the view controller, everything works as expected.
Be portrait EXCEPT when presenting a particular UIViewController subclass. This article helped a lot: ~~removed because of spam~~
set the Info.plist to support, portrait, landscape left, landscape right
implement application:supportedInterfaceOrientationsForWindow: like so:
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft;
}
// (I specifically want landscape left for the movie viewing)
subclass UINavigationController(the windows' rootViewController) and override like so:
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait; // enforces the “portrait everything” requirement
}
finally, I had to make sure the custom player view controller would "override" the supported orientation:
- (BOOL)shouldAutorotate
{
return NO;
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscape;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationLandscapeLeft;
}
Result:
UIViewController subclass presents but when dismissing the custom view controller, the presenting view controller is Portrait.
From fantageek article: "The system intersects the view controller’s supported orientations with the app’s supported orientations (as determined by the Info.plist file or the app delegate’s application:supportedInterfaceOrientationsForWindow: method) to determine whether to rotate."
I think you should change to this and try again
-(NSUInteger)supportedInterfaceOrientations {
if ([self.visibleViewController isKindOfClass:[MyVideoPlayerScreen class]])
{
return UIInterfaceOrientationMaskLandscape;
}
else
{
return UIInterfaceOrientationMaskPortrait;
}
}
Here's how you do it.
Implement these 3 methods on both presenting and presented controller:
- (BOOL)shouldAutorotate {
return NO; //-- for presented controller use YES
}
- (NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskLandscape; //-- any orientation you need
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeRight;
}
and now you can use in the presenting controller:
[self presentViewController:presentedController animated:true completion:nil];
This way, when you go back to presenting controller, it will have the correct orientation.
My case has 3 view controller:
- first view controller: portrait
- second view controller: landscape right (has navigation controller and was presented by first view controller)
- third view controller: portrait (has navigation controller and was pushed by second view controller )
And my solution had already here. Hope this helps
I have a problem with an app I am working on that goes like this:
The app's window has a rootViewController which is set to a custom class (MenuViewController) of UIViewController. This view controller has a rootViewController property of it's own. Whenever set this happens (really short version of the code):
- (void)setRootViewController:(UIViewControlelr *)rootViewController
{
...
_rootViewController = rootViewController;
if (self.rootViewController) {
[self addChildViewController:rootViewController];
[self.view addSubview:rootViewController.view];
}
...
}
Now this MenuViewController can show a modal view controller on top of it's rootViewController.
I do that like this:
[self.rootViewController presentModalViewController:viewController animated:YES completition:nil];
Everything looks to be ok until here. Now on iPad whenever I call [self.presentingViewController dismissViewControllerAnimated:YES completion:nil] from my modal view controller the interface beneath it rotates to the same orientation (that is UIInterfaceOrientationLandscapeLeft) regardless of what the initial orientation was when the view controller was presented.
So to conclude, my view's hierarchy is this:
Window
|
- Menu View Controller
|
- Root View Controller
|
- Modal view controller
Does anyone know how I can fix this? It doesn't happen on the iPhone.
It sounds to me as though on the iPad your MenuViewController's rootViewController supports multiple interface orientations, whereas on the iPhone it does not. This is speculation, as you have not said anything about this.
If this is the case, and the rootViewController does indeed support multiple interface orientations, the fix would be to override - supportedInterfaceOrientations and return portrait, which seems to be what you are suggesting you would want.
My app can autorotate but I need one of the views to only show in portrait mode and don't know how to achieve this.
I tried this (among other things) but the view in question still rotates:
-(BOOL)shouldAutorotate
{
return NO;
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
This solution explains how to control orientation on individual view controllers, provided they are managed by a navigation controller.
In Xcode 5, create a new file of type "Objective-C category", set it's "Category" to "rotation" and choose "UINavigationController" as "Category on".
A new file couple will appear in the project, having the following names:
UINavigationController+rotation.h
UINavigationController+rotation.m
In the .m file, write the following code:
- (BOOL) shouldAutorotate
{
return [[self topViewController] shouldAutorotate];
}
- (NSUInteger) supportedInterfaceOrientations
{
return [[self topViewController] supportedInterfaceOrientations];
}
This way, the navigation controller will let the current top view controller determine the orientation policy.
Then, in each specific view controller that is managed by the navigation controller, you can override the two orientation-related methods.
For instance, if a specific view controller shall appear in portrait orientation only:
- (BOOL) shouldAutorotate
{
return NO;
}
- (NSUInteger) supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
Make sure that the desired orientation is one of those set in the project deployment info.
Hope this is sufficiently detailed and can be of help.
supportedInterfaceOrientations will work if you present your view controller as a modal view controller. It won't work if you present it as part of a navigation controller stack. If you want your view presented modally but inside a navigation controller (to have navigation items, for instance) the solution I did was to subclass UINavigationController and override the supportedInterfaceOrientations methods on my subclass.
I am trying to follow a previous question in allowing a navigation controller view controllers in having different orientation rules. Previous Question
So for example, I have two view controllers the first is a Welcome the second Home. I would like the first view controller to only be Potrait and the second (Home) to allow both Port/Landscape.
I am not sure I quite understand in full how to complete this. Once I do, I intend to create a seperate project explaining how to do this and add to Github/share on the question for future reference.
In this particular project I am using a side view controller github project. PPRevealSideViewController.
My app Delegate is the following:
// Then we setup the reveal side view controller with the root view controller as the navigation controller
welcomeViewController = [[MESWelcomeViewController alloc] init];
UINavigationController *navController = [[MESNavViewControllerSubClass alloc] initWithRootViewController:welcomeViewController];
self.revealSideViewController = [[PPRevealSideViewController alloc] initWithRootViewController:navController];
[self.revealSideViewController setDirectionsToShowBounce:PPRevealSideDirectionNone];
[self.revealSideViewController setPanInteractionsWhenClosed:PPRevealSideInteractionContentView | PPRevealSideInteractionNavigationBar];
//self.window.rootViewController = welcomeViewController;
self.window.rootViewController = self.revealSideViewController;
[self.window makeKeyAndVisible];
From the above you can see I have subclassed the Navigation Controller as MESNavViewController. This is my head for this file:
#interface MESNavViewControllerSubClass : UINavigationController {
BOOL setLandscapeOK;
}
Imp file for MESNavViewController:
-(void)viewDidLoad {
NSLog(#"subclass called");
}
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
if (self->setLandscapeOK) {
// for iPhone, you could also return UIInterfaceOrientationMaskAllButUpsideDown
return UIInterfaceOrientationMaskAll;
}
return UIInterfaceOrientationMaskPortrait;
}
In my first (Welcome) View controller I have the following:
-(void)viewWillAppear {
BOOL setLandscapeOK = NO;
}
- (NSInteger)supportedInterfaceOrientations {
// Restriction for the welcome page to only allow potrait orientation
return UIInterfaceOrientationMaskPortrait;
}
In my second (Home) View controller I have only the following:
-(void)viewWillAppear {
BOOL setLandscapeOK = YES;
}
What I am seeing is both view controllers within the nav allow either orientation. I am not sure I quite understand it correctly. Hopefully I have provided enough information.
EDIT -----
I have updated the PPRevealSidePanel sub class which is the very top level controller. This then holds the nav controller, which in turn holds the view controller. The orientation should be decided by the view controller displayed.
PPRevealSidePanel Sub class -
Secondly I receive an error trying to update the setter setLandscapeOK for this sub class, on the actual view controller.
Login View controller -
Can you check the below thread which talks about handling orientation with nav controllers
NavController ShouldAutorotate
-anoop
Is your viewWillAppear method being called? The actual method is - (void)viewWillAppear:(BOOL)animated.
Aside from that, the issue is that supportedInterfaceOrientations is called before viewWillAppear. Try setting your BOOL flag in your initWithNibName method.
EDIT:
something like this:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.setLandscapeOK = YES;
}
return self;
}
EDIT 2:
I just noticed you're re-declaring setLandscapeOK as a new variable in viewWillAppear. This is hiding your superclass's instance of that variable. Try using self.setLandscapeOK instead of BOOL setLandscapeOK
EDIT 3:
Since you aren't subclassing your main nav controller, the above stuff wont work. You'll need an explicit way of notifying your navigation controller that it's subview doesnt want it to support certain orientations. When you push the viewcontroller that is portrait only, setLandscapeOK should be set to NO.
What goes wrong in your case is that you're overriding -supportedInterfaceOrientations in that UINavigationController subclass, so that method never gets passed to its child ViewControllers. Also you're declaring a new setLandscapeOK BOOL in viewWillAppear, not changing the global one. What is weird is that based on the code you've posted your app should be stuck in portrait, not allow all orientations, since you never seem to set the setLandscapeOK boolean to YES in the UINavigationController subclass.
But you seem to make things a lot more complicated than they should be. You shouldn't need that setLandscapeOK boolean and you shouldn't need to subclass UINavigationController or the PPRevealSideViewController just for the rotation issue. Both PPRevealSideViewController and UINavigationController are container ViewControllers, aka parent ViewControllers that contain child ViewControllers. By default when an orientation callback is called on the parent ViewController, it will call and return the same method of the child ViewController.
When -shouldAutoRotate, -supportedInterfaceOrientations or -shouldAutoRotateToInterfaceOrientation is called on the PPRevealSideViewController, it will call the same methods on its rootViewController, in your case a UINavigationController. The UINavigationController is also a container ViewController and will call the same method on the currently visible ViewController, in your case either the WelcomeViewController or the HomeViewController.
So, all you need to do is override -supportedInterfaceOrientations and -shouldAutoRotateToInterfaceOrientation in the WelcomeViewController (return portrait only) and the HomeViewController (return all). Don't override these methods in MESNavViewController, you shouldn't even need this subclass. You don't need to do anything else. You can remove that setLandscapeOK boolean in all of your ViewControllers as well.
When the application is in landscape mode (which I plan to force), displaying a modal view causes the parent view to rotate to portrait mode. If I set the return value of shouldAutoRotateToInterfaceOrientation to NO, the parent does not rotate, however the modal then slides in from the side and displays sideways. Below is the code that reveals the modal.
- (IBAction)loadExistingGame:(id)sender {
SavedGamesTableViewController *savedGames = [[SavedGamesTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
savedGames.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:savedGames animated:YES];
[savedGames release];
}
As per request here is the contents of the shouldAutoRotate method of the SavedGamesTableViewController
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Override to allow orientations other than the default portrait orientation.
return YES;
}
Ok I figured out what needed to be done to fix it. The plist file that contains a list of the possible orientations needs to be limited to a single landscape view. The parent to the modal table view needs to have the shouldAutoRotateToInterfaceOrientation method return YES only if the orientation matches the only orientation in the plist file.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
return interfaceOrientation = UIInterfaceOrientationLandscapeRight;
}
the modal viewcontroller should return NO for the same method.
Based on
When the application is in landscape
mode (which I plan to force),
displaying a modal view causes the
parent view to rotate to portrait
mode.
and
As per request here is the contents of
the shouldAutoRotate method of the
SavedGamesTableViewController
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Override to allow orientations other than the default portrait orientation.
return YES;
}
So what you're saying is that the parent view controller is not yet set to force only using landscape orientation, and when you show a modal view that is set to allow all orientations, you're wondering why your parent view rotates to portrait when you rotate the device to portrait? I don't understand your question... aren't you saying that parent view controller is currently set to allow rotation to portrait? Isn't this behaviour exactly what should happen?
I had a similar problem when bringing up a modal mail view. Forcing the rotation didn't work for me, but calling presentModalViewController on the application's main view controller rather than a child view controller solved the issue.
I was seeing the same behavior; in my case the problem was I had implemented shouldAutorotateToInterfaceOrientation to return YES unconditionally for the parent view controller but NOT for the presented modal view controller. So I suspect Shaggy Frog's comment is the key: whether you want to force landscape mode or not, you need to make sure that the two view controllers' shouldAutorotateToInterfaceOrientation implementations agree or weirdness will ensue.
UIViewController *vc = /* create view controller */;
UINavigationController *nc = nil;
if (IOS_VERSION_LESS_THAN_6_0) {
nc = [[MyCustomNavigationControllerSupportingAllOrientations alloc] initWithRootViewController:vc];
} else {
nc = [[UINavigationController alloc] initWithRootViewController:vc];
}
[self.navigationController presentModalViewController:nc animated:YES];
On iOS6 I use a UINavigationController.
On pre-iOS6 I subclass UINavigationController, like this:
#interface MyCustomNavigationControllerSupportingAllOrientations : UINavigationController
#end
#implementation MyCustomNavigationControllerSupportingAllOrientations
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
#end