I searched the web high and low, but couldn't find a definitive answer on this. What's the best way to have only one UIViewController support landscape mode when it's embedded in a UINavigationController, which itself is part of a UITabBarController?
Most solutions, like this one, suggest overriding
-(NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
in the AppDelegate. This works somewhat, but when returning from the only ViewController which supports landscape mode, all the other (non-landscape ViewControllers) will be in landscape orientation as well. They don't keep their portrait orientation.
I've seen apps getting this right, so I know that it must be possible somehow. For instance, movie player apps are often portrait-only, but the actual movie player view is presented modally in forced-landscape mode. Upon dismissing the modal viewcontroller, the underlying viewcontroller is still correctly in portrait orientation,
Any hints?
Here's my conclusion after a lot of research:
First, I tried implementing
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window;
in the AppDelegate as described here, which worked for most cases. But apart from feeling quite "hacky", there was one serious gotcha: The workaround for modally displayed view controllers (see section "A small problem with modal controllers") breaks down when, for instance, displaying an AVPlayerViewController because it implements its own dismiss method and you can't hook into it to set self.isPresented (unfortunately, viewWillDisappear: is too late).
So I went with the alternative approach of using subclasses for UITabBarController and UINavigationController which feels much cleaner and only slightly more verbose:
CustomNavigationController
CustomNavigationController.h
#import <UIKit/UIKit.h>
#interface CustomNavigationController : UINavigationController
#end
CustomNavigationController.m
#import "CustomNavigationController.h"
#implementation CustomNavigationController
- (BOOL)shouldAutorotate
{
return [self.topViewController shouldAutorotate];
}
- (NSUInteger)supportedInterfaceOrientations
{
return [self.topViewController supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [self.topViewController preferredInterfaceOrientationForPresentation];
}
#end
CustomTabBarController
CustomTabBarController.h
#import <UIKit/UIKit.h>
#interface CustomTabBarController : UITabBarController
#end
CustomTabBarController.m
#import "CustomTabBarController.h"
#implementation CustomTabBarController
- (BOOL)shouldAutorotate
{
return [self.selectedViewController shouldAutorotate];
}
- (NSUInteger)supportedInterfaceOrientations
{
return [self.selectedViewController supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [self.selectedViewController preferredInterfaceOrientationForPresentation];
}
#end
After that it's just a matter of adding the following code to your UIViewControllers:
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
// Presents the `UIViewController` in landscape orientation when it is first displayed
return UIInterfaceOrientationLandscapeLeft;
}
- (NSUInteger)supportedInterfaceOrientations {
// Allows all other orientations (except upside-down)
return UIInterfaceOrientationMaskAllButUpsideDown;
}
Now you may decide the preferred and supported orientations on a per-view-controller-basis. Note that I didn't implement shouldAutorotate in my view controllers because it defaults to YES, which is what you want if your view controllers should be forced to a certain orientation (yes, they should autorotate to the only supported orientation).
The call chain goes something like this:
The CustomTabBarController, being the window's rootViewController, is asked for supported/preferred orientations
The CustomTabBarController in turn asks its selectedViewController (view controller of the currently selected tab), which happens to be my CustomNavigationController
The CustomNavigationController asks the embedded topViewController, which is finally the actual UIViewController implementing the methods above
You still need to allow all device orientations in your build target. And of course, you need to update your storyboards/xibs/classes to use these subclasses instead of the standard UINavigationController and UITabBarController classes.
The only downside to this approach is that, at least in my case, I had to add these methods to all of my view controllers to make most view controllers portrait-only and some landscape-capable. But IMHO it's definitely worth it!
You should write the above code to the UINavigationController subclass. You should define your application orientation and separate your view controller orientation using if statements.
-(BOOL)shouldAutorotate {
if ([[self.viewControllers lastObject] isKindOfClass:[UIViewController class]] ) {
return [[self.viewControllers lastObject] shouldAutorotate];
}
return NO;
}
- (NSUInteger)supportedInterfaceOrientations {
if ([[self.viewControllers lastObject] isKindOfClass:[UIViewController class]]) {
return [[self.viewControllers lastObject] supportedInterfaceOrientations];
}
return UIInterfaceOrientationMaskPortrait;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
I am trying to set the orientation for a UIViewController and disallow all orientation changes on that UIViewController. It is important to note that the app allows all orientations, but on this one view controller I want to allow only portrait orientation.
I am working on iOS 8, the app works with iOS 6 and up. The described behavior is happening in iOS 7 and 8. I cannot test it in iOS 6 so I am not sure if it occurs in iOS 6.
I have the following methods implemented:
- (BOOL)shouldAutorotate
{
return NO; // I've also tried returning YES - no success
}
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait; // I've also tried UIInterfaceOrientationPortrait - no success
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return UIInterfaceOrientationMaskPortrait; // Never called
}
I have been able to set the orientation for a view controller that is presented modally, but I am not presenting the view controller modally for this project. Can this even be done?
How do I specify the orientation for one UIViewController, without effecting the orientation of other UIViewControllers? Can this even be done?
Supposing that your view controller is embedded in a navigation controller, you need to use a custom subclass of UINavigationController and add:
- (BOOL)shouldAutorotate;
{
BOOL shouldAutorotate;
if ([self.topViewController respondsToSelector:#selector(shouldAutorotate)])
{
shouldAutorotate = [self.topViewController shouldAutorotate];
}
else {
shouldAutorotate = [super shouldAutorotate];
}
return shouldAutorotate;
}
- (NSUInteger)supportedInterfaceOrientations
{
NSUInteger supportedOrientations;
if ([[self topViewController] respondsToSelector:#selector(supportedInterfaceOrientations)]) {
supportedOrientations = [[self topViewController] supportedInterfaceOrientations];
}
else {
supportedOrientations = [super supportedInterfaceOrientations];
}
return supportedOrientations;
}
in your navigation controller. The methods in your view controller are just fine, so leave them as they are. That works in iOS 7-8 (haven't tested in iOS6)
I'm using navigationController to push someone else's view controller. I can use shouldAutorotate to disable rotation in my view, but this doesn't work for the pushed view, which was implemented by someone else and I can not touch. Is there any way to achieve this? for example by disabling any rotation in any subview of my navigation controller?
Thanks!
Push your own subclass of the vc, one that implements supportedInterfaceOrientations like this:
// .h
#import "ThirdPartyVC.h"
#interface MyNonRotatingThirdPartyVC : ThirdPartyVC
#end
// .m
#implementation MyNonRotatingThirdPartyVC
- (NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
Allocate and present a MyNonRotatingThirdPartyVC instead of the original
.
I have an iPad application which I have created using storyboards. I have created another single viewController which I have created using a separate .xib file. This viewController I need to call from the main application, and then later dismiss to return back to the main application. I am able to do this so far. My problem is that because I am using a Navigation Controller to call this secondary view controller, I am unable to load this view controller in landscape mode. I am only able to load it in portrait mode. Based on going through this forum, and from whatever research that I have done, I have learned that I need to subclass the navigation controller, and then that is how I will be able to load this secondary view controller in landscape mode.
I have included the following methods in my secondary view controller (NextViewController), but it has no effect:
-(BOOL)shouldAutorotate
{
return YES;
}
-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscape;
}
Here is the code in the calling viewController (MainViewController), which calls NextViewController, which in turn is appearing in portrait mode, instead of the desired landscape mode:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_nextView = [[NextLandscapeViewController alloc] initWithNibName:#"NextLandscapeViewController" bundle:nil];
[_nextView setDelegate:(id)self];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:_nextView];
[self presentViewController:navigationController animated:YES completion:nil];
}
As I pointed out, the solution that I need is to subclass the Navigation Controller, but I honestly have never done this before, and nor do I know how to do it. Can someone show me how to do it so that I can call NextViewController, and have it displayed in landscape mode?
Thanks in advance to all who reply.
For subclass from Navigation Controller for orientation, you can try this code (as example):
// .h - file
#interface MyNavigationController : UINavigationController
#end
// .m - file
#import "MyNavigationController.h"
#implementation MyNavigationController
-(BOOL)shouldAutorotate
{
return [self.topViewController shouldAutorotate];
}
-(NSUInteger)supportedInterfaceOrientations
{
return [self.topViewController supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [self.topViewController preferredInterfaceOrientationForPresentation];
}
#end
upd: (This code work on ios6)
I have an iPhone app that uses a UINavigationController to present a drill-down interface: First one view, then another, up to four levels deep. I want the first three views restricted to portrait orientation and only the last view should be allowed to rotate to landscape. When returning from the fourth view to the third and the fourth view was in landscape orientation I want everything to rotate back to portrait.
In iOS 5 I simply defined shouldAutorotateToInterfaceOrientation: in each of my view controllers to return YES for the allowable orientations. Everything worked as described above, including the return to portrait even if the device was being held in landscape orientation when returning from view controller #4 to #3.
In iOS 6 all view controllers rotate to landscape, breaking those that weren't meant to. The iOS 6 release notes say
More responsibility is moving to the app and the app delegate. Now, iOS containers (such as UINavigationController) do not consult their children to determine whether they should autorotate. [...] The system asks the top-most full-screen view controller (typically the root view controller) for its supported interface orientations whenever the device rotates or whenever a view controller is presented with the full-screen modal presentation style. Moreover, the supported orientations are retrieved only if this view controller returns YES from its shouldAutorotate method. [...] The system determines whether an orientation is supported by intersecting the value returned by the app’s supportedInterfaceOrientationsForWindow: method with the value returned by the supportedInterfaceOrientations method of the top-most full-screen controller.
So I subclassed UINavigationController, gave my MainNavigationController a boolean property landscapeOK and used this to return the allowable orientations in supportedInterfaceOrientations. Then in each of my view controllers' viewWillAppear: methods I have a line like this
[(MainNavigationController*)[self navigationController] setLandscapeOK:YES];
to tell my MainNavigationController the desired behavior.
Here comes the question: If I now navigate to my fourth view in portrait mode and turn the phone over it rotates to landscape. Now I press the back button to return to my third view which is supposed to work portrait only. But it doesn't rotate back. How do I make it do that?
I tried
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait]
in the viewWillAppear method of my third view controller, but it doesn't do anything. Is this the wrong method to call or maybe the wrong place to call it or should I be implementing the whole thing in a totally different way?
I had the same problem and found a solution that works for me.
To make it work, it is not sufficient to implement - (NSUInteger)supportedInterfaceOrientations in your UINavigationController.
You also need to implement this method in your controller #3, which is the first one to be portrait-only after popping controller #4.
So, I have the following code in my UINavigationController:
- (BOOL)shouldAutorotate
{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations
{
if (self.isLandscapeOK) {
// for iPhone, you could also return UIInterfaceOrientationMaskAllButUpsideDown
return UIInterfaceOrientationMaskAll;
}
return UIInterfaceOrientationMaskPortrait;
}
In view controller #3, add the following:
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
You don't need to add anything to your view controllers #1, #2, and #4.
This works for me, I hope it will help you.
Add a CustomNavigationController
Override these methods in it:
-(BOOL)shouldAutorotate
{
return [[self.viewControllers lastObject] shouldAutorotate];
}
-(NSUInteger)supportedInterfaceOrientations
{
return [[self.viewControllers lastObject] supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [[self.viewControllers lastObject] preferredInterfaceOrientationForPresentation];
}
Now add all orientations in the plist
In the view controller add only the required ones:
-(BOOL)shouldAutorotate
{
return YES;
}
-(NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
these methods override the navigation controller methods
After looking through every answer in countless similar questions on SO, none of the answers worked for me, but they did give me some ideas. Here's how I ended up solving the problem:
First, make sure your Supported Interface Orientations in your project's target contain all orientations that you want for your rotating view.
Next, make a category of UINavigationController (since Apple says not to subclass it):
#implementation UINavigationController (iOS6AutorotationFix)
-(BOOL)shouldAutorotate {
return [self.topViewController shouldAutorotate];
}
#end
Import that category and the view controller that you want to be able to rotate (which I'll call RotatingViewController) to your highest level view controller, which should contain your navigation controller. In that view controller, implement shouldAutorotate as follows. Note that this should not be the same view controller that you want to rotate.
-(BOOL)shouldAutorotate {
BOOL shouldRotate = NO;
if ([navigationController.topViewController isMemberOfClass:[RotatingViewController class]] ) {
shouldRotate = [navigationController.topViewController shouldAutorotate];
}
return shouldRotate;
}
Finally, in your RotatingViewController, implement shouldAutorotate and supportedInterfaceOrientations as follows:
-(BOOL)shouldAutorotate {
// Preparations to rotate view go here
return YES;
}
-(NSUInteger)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskAllButUpsideDown; // or however you want to rotate
}
The reason you need to do this is because iOS 6 gives control of rotation to the root view controller instead of the top view controlller. If you want an individual view's rotation to behave differently than other views in the stack, you need to write a specific case for it in the root view controller.
I don't have enough reputation to comment on #Brian's answer so I'll add my note here.
Brian mentioned that iOS6 gives the rotation control to the rootViewController - this could not only be a UINavigationController as mentioned but also a UITabBarController, which it was for me. My structure looks like this:
UITabBarController
UINavigationController
UIViewControllers ...
UINavigationController
UIViewControllers ...
So I added the methods first in a custom UITabBarController, then in a custom UINavigationController and then lastly in the specific UIViewController.
Example from the UITabBarController and UINavigationController:
- (BOOL)shouldAutorotate {
return [self.viewControllers.lastObject shouldAutorotate];
}
- (NSUInteger)supportedInterfaceOrientations {
return [self.viewControllers.lastObject supportedInterfaceOrientations];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return [self.viewControllers.lastObject shouldAutorotateToInterfaceOrientation:toInterfaceOrientation];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return [self.viewControllers.lastObject preferredInterfaceOrientationForPresentation];
}
I'd like to give a partial answer to my own question. I found the following line of code, used in the viewWillAppear method of my third UIViewController, to work:
[[UIDevice currentDevice]
performSelector:NSSelectorFromString(#"setOrientation:")
withObject:(id)UIInterfaceOrientationPortrait];
But I don't really like this solution. It is using a trick to assign to a read-only property which according to Apple documentation represents the physical orientation of the device. It's like telling the iPhone to jump to the correct orientation in the hand of the user.
I am very tempted to leave this in my app since it simply works. But it doesn't feel right so I'd like to leave the question open for a clean solution.
This is I am using for orientation support in ios 6.0
-(BOOL)shouldAutorotate{
return YES;
}
- (NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationPortrait;
}
Being that this is a highly viewed thread. I thought I would add what I believe is the easiest answer. This works for ios8 and up
-(BOOL)shouldAutorotate
{
return YES;
}
and
-(UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskPortrait;
}
That's it. Enjoy!
Oh, and my ViewControllers are embedded in a navigation controller, which I did not need to subclass or configure in any way.
This might not work for everyone, but it works great for me. Instead of implementing...
[(MainNavigationController*)[self navigationController] setLandscapeOK:YES];
in viewWillAppear in all of my controllers, I decided to centralize this process inside of my UINavigationController subclass by overriding the UINavigationControllerDelegate method navigationController:willShowViewController:animated:
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
self.previousVCLandscapeOK = self.isLandscapeOK; // Store the current VC's orientation preference before pushing on the new VC so we can set this again from within the custom "back" method
self.isLandscapeOK = NO; // Set NO as default for all VC's
if ([viewController isKindOfClass:[YourViewController class]]) {
self.isLandscapeOK = YES;
}
}
I have found that this delegate method doesn't get called when popping a VC off of the nav stack. This wasn't an issue for me because I am handling the back functionality from within my UINavigationController subclass so that I can set the proper navigation bar buttons and actions for specific VC's like this...
if ([viewController isKindOfClass:[ShareViewController class]]) {
UIButton* backButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 57, 30)];
[backButton setImage:[UIImage imageNamed:#"back-arrow"] forState:UIControlStateNormal];
[backButton addTarget:self action:#selector(back) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem* backButtonItem = [[UIBarButtonItem alloc] initWithCustomView:backButton];
viewController.navigationItem.leftBarButtonItem = backButtonItem;
UIImageView* shareTitle = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"share-title"]];
[shareTitle setContentMode:UIViewContentModeScaleAspectFit];
[shareTitle setFrame:CGRectMake(0, 0, shareTitle.frame.size.width - 10, shareTitle.frame.size.height - 10)];
viewController.navigationItem.titleView = shareTitle;
} else if(...) {
...
}
Here is what my back method looks like to handle popping the VC off of the stack and to set the appropriate rotation preference...
- (void)back {
self.isLandscapeOK = self.previousVCLandscapeOK;
self.previousVCLandscapeOK = NO;
[self popViewControllerAnimated:YES];
}
So, as you can see, basically all that is happening is I'm first setting me two properties...
#property (nonatomic) BOOL isLandscapeOK;
#property (nonatomic) BOOL previousVCLandscapeOK;
in navigationController:willShowViewController:animated: which will determine what the supported orientations are within that VC that is about to be presented. When popping a VC, my custom "back" method is being called and I'm then setting the isLandscapeOK to what was stored via the previousVCLandscapeOK value.
As I said, this might not work for everyone, but it works great for me and I don't have to worry about adding code to each of my view controllers, I was able to keep it all centralized in the UINavigationController subclass.
Hope this helps someone as it did me.
Thanks, Jeremy.
Go to you Info.plist file and make the change
You want to Force iOS 6 app portrait only then you can add to a UIViewController subclass below methods
- (BOOL)shouldAutorotate {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return YES;
} else {
return NO;
}
}
- (NSUInteger)supportedInterfaceOrientations {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
} else {
return UIInterfaceOrientationMaskPortrait;
}
}
I wanted to have all my VCs locked to portrait orientation except one. This is what worked for me.
Add support for all orientations in the plist file.
In the root view controller, detect the kind of view controller thats on top of the window and set the orientation of the app accordingly in the supportedInterfaceOrientations method. For example, I needed my app to rotate only when the webview was on top of the stack. Here's what I added in my rootVC :
-(NSUInteger)supportedInterfaceOrientations
{
UIViewController *topMostViewController = [[Utils getAppDelegate] appNavigationController].topViewController;
if ([topMostViewController isKindOfClass:[SVWebViewController class]]) {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
return UIInterfaceOrientationMaskPortrait;
}
I don't have enough reputation to answer #Ram S question under #micmdk reply so I'll add my note here.
When you use UITabbarController, try to change self.viewControllers.lastObject in
#micmdk's code to self.selectedViewController like this:
- (BOOL)shouldAutorotate {
return [self.selectedViewController shouldAutorotate];
}
- (NSUInteger)supportedInterfaceOrientations {
return [self.selectedViewController supportedInterfaceOrientations];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
return [self.selectedViewController shouldAutorotateToInterfaceOrientation:toInterfaceOrientation];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return [self.selectedViewController preferredInterfaceOrientationForPresentation];
}
You can implement and override shouldAutorotate() and supportedInterfaceOrientations var in all of your View Controller classes that should be presented in a different orientations than ones defined in PLIST of your app.
However, in a nontrivial User Interface you might face a problem to add it in dozens of classes and you do not want to make all of them subclass of several common that supports it (MyBaseTableViewController, MyBaseNavigationController and MyBaseTabBarController).
Since you cannot override those method/var on UIViewController directly, you may do that on its subclasses that are typically base classes of yours like UITableViewController, UINavigationController and UITabBarController.
So you may implement a few extensions and still setup MyPreciousViewController to show in a different orientations than all others like this Swift 4 code snippet:
extension UITableViewController {
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if let last = self.navigationController?.childViewControllers.last,
last != self {
return last.supportedInterfaceOrientations
} else {
return [.portrait]
}
}
}
extension MyPreciousViewController {
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return [.portrait,.landscape]
}
}
extension UINavigationController {
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return [.portrait]
}
}
extension UITabBarController {
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return [.portrait]
}
}
I've solved the same kind of issue.
If you are using the UINavigationController to push the view controllers, you have to set the methods below.
extension UINavigationController{
override open var shouldAutorotate: Bool {
if topViewController != nil && (topViewController?.isKind(of: LogInViewController.self))!
{
return true
}
return false
}
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if topViewController != nil && (topViewController?.isKind(of: LogInViewController.self))!
{
return .portrait
}
return .landscapeRight
}
override open var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
if topViewController != nil && (topViewController?.isKind(of: LogInViewController.self))!
{
return .portrait
}
return .landscapeRight
}
}
In place of LoginViewController use which UIViewController you want show. In my case, I want to show the LoginViewController in the Portrait mode other ViewControllers in landscape mode.
Please use the following method to solve this issue
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
return only the orientation you want!!!