UISplitViewController - prevent splitting in landscape on iPhone 6 plus - ios

I am using a UISplitViewController in my app. This works just fine on iPad where primary and secondary are always visible, and it works just fine on most iPhones where it acts like a UINavigationController.
On iPhone 6+ and 6S+ the split view acts like an iPhone in portrait and like an iPad in landscape. This splitting in landscape is causing me problems and I'd like to avoid it.
Is there any way to prevent the UISplitViewController from showing primary and secondary controllers in iPhone 6+ landscape? I just want it to show the secondary controller, the same as it would do for other iPhones.
Thanks.

I was able to do this by subclassing the UISplitViewController and then overriding the trait collection to return a compact horizontal size class when the device is not an iPad. I know checking the interface idiom is a faux-pas these days, but I wasn't sure how else to do it.
I simply added this method to my UISplitViewController subclass:
-(UITraitCollection *)traitCollection {
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
return [super traitCollection];
} else {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
}
Any suggestions for a better way to do this are certainly welcome.

Here is the same answer in Swift but also with a fix where the vertical size class would be wrong on phone in landscape:
override var traitCollection: UITraitCollection {
if UI_USER_INTERFACE_IDIOM() == .pad {
return super.traitCollection
} else {
let horizontal = UITraitCollection(horizontalSizeClass: .compact)
let vertical = UITraitCollection(verticalSizeClass: super.traitCollection.verticalSizeClass)
return UITraitCollection.init(traitsFrom: [horizontal, vertical])
}
}

I had some issues with UINavigationControllers not displaying correctly with the code above. This is the method that worked for me (Swift 5):
1) Create a UIViewController containing a UIContainerView
2) Embed your UISplitViewController in that container
3) Add the following code:
class SplitViewContainerViewController: UIViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UI_USER_INTERFACE_IDIOM() != .pad {
performOverrideTraitCollection()
}
}
private func performOverrideTraitCollection() {
for childVC in self.children {
setOverrideTraitCollection(UITraitCollection(horizontalSizeClass: .compact), forChild: childVC)
}
}
}
4) Set the view controller containing the container view to be SplitViewContainerViewController
Update For iOS 13
The code above no longer works on iOS 13. Use the following instead on the SplitViewContainerViewController class:
override func overrideTraitCollection(forChild childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom != .pad {
return UITraitCollection(horizontalSizeClass: .compact)
} else {
return super.traitCollection
}
}

Related

Detecting orientation on loading

When my simulator is in portrait and when my viewcontroller loads initially, it prints out Landscape instead of Portrait but when I change the orientaiton, it correctly displays the orientation so forth. I did the following
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if (UIDevice.current.orientation == UIDeviceOrientation.portrait || UIDevice.current.orientation == UIDeviceOrientation.portraitUpsideDown)
{
print("Portrait")
}
else
{
print("Landscape")
}
}
I have no idea why it is displaying wrong orientation when it loads initially but once I rotate everything seems to work.
P.S. It seems like when simulator initially loads, the orientaiton is unknown, so it is choosing the else condition, how to avoid this from happening and identify the correct orientaiton?
You can check current orientation by UIApplication.shared.statusBarOrientation.isPortrait or UIApplication.shared.statusBarOrientation.isLandscape.
if UIDevice.currentDevice().orientation.isLandscape.boolValue {
print("landscape")
} else {
print("portrait")
}

Support landscape orientation for horizontalSizeClass=.Regular in iOS

I'm trying to build an app that supports portrait AND landscape orientations for iOS devices with regular horizontal size class, and portrait only for the rest.
At the time of this writing, it would be: Portrait only (for iPhones except 6 Plus/6s Plus) and Portrait AND Landscape for iPhone 6 Plus / 6s Plus, and iPad.
This is a similar behavior performed by the native Mail app.
I've tried among other things, the following:
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return [.Portrait, .LandscapeLeft, .LandscapeRight]
}
override func shouldAutorotate() -> Bool {
return (traitCollection.horizontalSizeClass == .Regular)
}
However, shouldAutororate is obviously called before the interface rotates, so that happens before traitCollection gets updated.
So, the question is, how to achieve this? I'm trying to accomplish this in the cleanest way possible without referencing explicitly userInterfaceIdiom, screen size, etc.
override var supportedInterfaceOrientations:UIInterfaceOrientationMask{
return [.portrait, .landscapeLeft, .landscapeRight]
}
override var shouldAutorotate:Bool {
return (traitCollection.horizontalSizeClass == .regular) || (traitCollection.displayScale > 2);
}

Different size classes for iPad portrait and landscape modes with containerviews

I've found this question and I've tried to implement the solution that has been given. However I run into a problem.
My initial view controller has two container views who both have their own view controller. I've created a root view controller that is assigned to the initial view controller. The code in this class looks like this.
class RootViewController: UIViewController {
var willTransitionToPortrait: Bool!
var traitCollection_CompactRegular: UITraitCollection!
var traitCollection_AnyAny: UITraitCollection!
override func viewDidLoad() {
super.viewDidLoad()
setupReferenceSizeClasses()
// Do any additional setup after loading the view.
}
override func viewWillAppear(animated: Bool) {
willTransitionToPortrait = self.view.frame.size.height > self.view.frame.size.width
}
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
willTransitionToPortrait = size.height > size.width
}
func setupReferenceSizeClasses(){
let traitCollection_hCompact = UITraitCollection(horizontalSizeClass: .Compact)
let traitCollection_vRegular = UITraitCollection(verticalSizeClass: .Regular)
traitCollection_CompactRegular = UITraitCollection(traitsFromCollections: [traitCollection_hCompact, traitCollection_vRegular])
let traitCollection_hAny = UITraitCollection(horizontalSizeClass: .Unspecified)
let traitCollection_vAny = UITraitCollection(verticalSizeClass: .Unspecified)
traitCollection_AnyAny = UITraitCollection(traitsFromCollections: [traitCollection_hAny, traitCollection_vAny])
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = ((willTransitionToPortrait) != nil) ? traitCollection_CompactRegular : traitCollection_AnyAny
return traitCollectionForOverride;
}
However when I run it the size class won't respons like it should. One of the container view controllers will start acting weird in both landscape and portrait mode like can be seen below.
When I don't assign the rootviewcontroller it will look like this
While it should look like this in portrait mode
Does anyone know what might be going wrong here? Why it doesn't change the size class like desired.
EDIT
Like #Muhammad Yawar Ali asked here are screenshots from the position of all the size classes I've set. I have no warnings or errors on any constraints so these screenshots contain the updated views.
I hope this shows everything that is needed.
EDIT:
for some reason I'm unable to put in all the screenshots
On the viewWillTransitionToSize you need to call also super, to pass the event to the next responder (your rootviewcontroller)...
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
willTransitionToPortrait = size.height > size.width
}
Realize this is over two years old but...
I just ran across what I think is a similar issue. What you may be forgetting is that 'overrideTraitCollectionForChildViewController' only overrides the views children, so this method won't do anything with the containers since they are located at the root.
I solved this putting my two containers in a UIStackView in Interface Builder and made a property of this stack in code and then updated the axis depending on the orientation. For example, in Objective-C:
#property (weak, nonatomic) IBOutlet UIStackView *rootStack;
// ...
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (UI_USER_INTERFACE_IDIOM() != UIUserInterfaceIdiomPad) {
return [super overrideTraitCollectionForChildViewController:childViewController];
}
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
self.rootStack.axis = UILayoutConstraintAxisVertical;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
}
else {
self.rootStack.axis = UILayoutConstraintAxisHorizontal;
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
If you have any constraints that are different between portrait and landscape you will need to adjust those in code as well.
I suppose you could also solve this by embedding the view controller with the containers in another view controller.
I have cloned your code from repository : https://github.com/MaikoHermans/sizeClasses.git
And editted code put the below code in you controller it will work fine & will not effect your design in iPads.
import UIKit
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
if view.bounds.width < view.bounds.height {
return UITraitCollection(horizontalSizeClass: .Unspecified)
} else {
return UITraitCollection(horizontalSizeClass: .Regular)
}
}
}
You can try with this code but There is an issue i believe its not updating traits properly for ipads and view layout remains same but looks good. I have tried multiple ways but not succeeded yet will update my answer.

iPad Landscape and Portrait different layouts with Size Class

How to design iPad Landscape and Portrait screens with different Layouts using Size class.
I could find only w-regular and h-regular for both orientations. Example: I need to align 2 views vertically in portrait and horizontally in landscape using Size Class
Finally I found a solution :
if traitCollection.verticalSizeClass == .Regular && traitCollection.horizontalSizeClass == .Regular {
var orientation:UIInterfaceOrientation = UIApplication.sharedApplication().statusBarOrientation;
if orientation == UIInterfaceOrientation.LandscapeLeft || orientation == UIInterfaceOrientation.LandscapeRight {
// orientation is landscape
} else {
// orientation is portrait
}
}
It appears to be Apple's intent to treat both iPad orientations as the same -- but as a number of us are finding, there are very legitimate design reasons to want to vary the UI layout for iPad Portrait vs. iPad Landscape.
However, please see this answer for another approach to adapting size classes to do what we need:
https://stackoverflow.com/a/28268200/4517929
For Swift 3 it would look like this:
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UI_USER_INTERFACE_IDIOM() == .pad &&
view.bounds.width > view.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
It will use wRhC instead of wRhR for iPad devices in landscape mode.
Put this code to your base view controller, i.e. this rule will work for all controllers that were presented by this one.
You can put any additional conditions here... For example, if you want this rule to be working only for specific view controller, your if operator would look like this:
if UI_USER_INTERFACE_IDIOM() == .pad &&
childViewController is YourSpecificViewController &&
view.bounds.width > view.bounds.height {
let collections = [UITraitCollection(horizontalSizeClass: .regular),
UITraitCollection(verticalSizeClass: .compact)]
return UITraitCollection(traitsFrom: collections)
}
Swift 4
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.orientation.isLandscape {
return UITraitCollection(traitsFrom:[UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .regular)])
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
I like to create a custom subclass of navigationController and then set a storyboards initial Navigation controller to that class. You can also do something similar with a ViewController.
Example:
import UIKit
class NavigationControllerWithTraitOverride: UINavigationController {
// If you make a navigationController a member of this class the descendentVCs of that navigationController will have their trait collection overridden with compact vertical size class if the user is on an iPad and the device is horizontal.
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
if UIDevice.current.userInterfaceIdiom == .pad && UIDevice.current.orientation.isLandscape {
return UITraitCollection(traitsFrom:[UITraitCollection(verticalSizeClass: .compact), UITraitCollection(horizontalSizeClass: .regular)])
}
return super.overrideTraitCollection(forChildViewController: childViewController)
}
}
Note: You should not override traitCollection as per the docs
Important
Use the traitCollection property directly. Do not override it. Do not provide a custom implementation.

How to force view controller orientation in iOS 8?

Before iOS 8, we used below code in conjunction with supportedInterfaceOrientations and shouldAutoRotate delegate methods to force app orientation to any particular orientation. I used below code snippet to programmatically rotate the app to desired orientation. Firstly, I am changing the status bar orientation. And then just presenting and immediately dismissing a modal view rotates the view to desired orientation.
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeRight animated:YES];
UIViewController *c = [[UIViewController alloc]init];
[self presentViewController:vc animated:NO completion:nil];
[self dismissViewControllerAnimated:NO completion:nil];
But this is failing in iOS 8. Also, I have seen some answers in stack overflow where people suggested that we should always avoid this approach from iOS 8 onwards.
To be more specific, my application is a universal type of application. There are three controllers in total.
First View controller- It should support all orientations in iPad and only portrait (home button down) in iPhone.
Second View controller- It should support only landscape right in all conditions
Third View controller- It should support only landscape right in all conditions
We are using navigation controller for page navigation. From the first view controller, on a button click action, we are pushing the second one on stack. So, when the second view controller arrives, irrespective of device orientation, the app should lock in landscape right only.
Below is my shouldAutorotate and supportedInterfaceOrientations methods in second and third view controller.
-(NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskLandscapeRight;
}
-(BOOL)shouldAutorotate {
return NO;
}
Is there any solution for this or any better way of locking a view controller in particular orientation for iOS 8. Please help!!
For iOS 7 - 10:
Objective-C:
[[UIDevice currentDevice] setValue:#(UIInterfaceOrientationLandscapeLeft) forKey:#"orientation"];
[UINavigationController attemptRotationToDeviceOrientation];
Swift 3:
let value = UIInterfaceOrientation.landscapeLeft.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
Just call it in - viewDidAppear: of the presented view controller.
Orientation rotation is a little more complicated if you are inside a UINavigationController or UITabBarController. The problem is that if a view controller is embedded in one of these controllers the navigation or tab bar controller takes precedence and makes the decisions on autorotation and supported orientations.
I use the following 2 extensions on UINavigationController and UITabBarController so that view controllers that are embedded in one of these controllers get to make the decisions.
Give View Controllers the Power!
Swift 2.3
extension UINavigationController {
public override func supportedInterfaceOrientations() -> Int {
return visibleViewController.supportedInterfaceOrientations()
}
public override func shouldAutorotate() -> Bool {
return visibleViewController.shouldAutorotate()
}
}
extension UITabBarController {
public override func supportedInterfaceOrientations() -> Int {
if let selected = selectedViewController {
return selected.supportedInterfaceOrientations()
}
return super.supportedInterfaceOrientations()
}
public override func shouldAutorotate() -> Bool {
if let selected = selectedViewController {
return selected.shouldAutorotate()
}
return super.shouldAutorotate()
}
}
Swift 3
extension UINavigationController {
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return visibleViewController?.supportedInterfaceOrientations ?? super.supportedInterfaceOrientations
}
open override var shouldAutorotate: Bool {
return visibleViewController?.shouldAutorotate ?? super.shouldAutorotate
}
}
extension UITabBarController {
open override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if let selected = selectedViewController {
return selected.supportedInterfaceOrientations
}
return super.supportedInterfaceOrientations
}
open override var shouldAutorotate: Bool {
if let selected = selectedViewController {
return selected.shouldAutorotate
}
return super.shouldAutorotate
}
}
Now you can override the supportedInterfaceOrientations method or you can override shouldAutoRotate in the view controller you want to lock down otherwise you can leave out the overrides in other view controllers that you want to inherit the default orientation behavior specified in your app's plist
Disable Rotation
class ViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
}
Lock to Specific Orientation
class ViewController: UIViewController {
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
}
In theory this should work for all complex view controller hierarchies, but I have noticed an issue with UITabBarController. For some reason it wants to use a default orientation value. See the following blog post if you are interested in learning about how to work around some of the issues:
Lock Screen Rotation
This is what worked for me:
https://developer.apple.com/library//ios/documentation/UIKit/Reference/UIViewController_Class/index.html#//apple_ref/occ/clm/UIViewController/attemptRotationToDeviceOrientation
Call it in your viewDidAppear: method.
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIViewController attemptRotationToDeviceOrientation];
}
I found that if it's a presented view controller, you can override preferredInterfaceOrientationForPresentation
Swift:
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.LandscapeLeft
}
override func shouldAutorotate() -> Bool {
return false
}
This way work for me in Swift 2 iOS 8.x:
PS (this method dont require to override orientation functions like shouldautorotate on every viewController, just one method on AppDelegate)
Check the "requires full screen" in you project general info.
So, on AppDelegate.swift make a variable:
var enableAllOrientation = false
So, put also this func:
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if (enableAllOrientation == true){
return UIInterfaceOrientationMask.All
}
return UIInterfaceOrientationMask.Portrait
}
So, in every class in your project you can set this var in viewWillAppear:
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.enableAllOrientation = true
}
If you need to make a choices based on the device type you can do this:
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
switch UIDevice.currentDevice().userInterfaceIdiom {
case .Phone:
// It's an iPhone
print(" - Only portrait mode to iPhone")
appDelegate.enableAllOrientation = false
case .Pad:
// It's an iPad
print(" - All orientation mode enabled on iPad")
appDelegate.enableAllOrientation = true
case .Unspecified:
// Uh, oh! What could it be?
appDelegate.enableAllOrientation = false
}
}
First of all - this is a bad idea, in general, something wrong going with your app architecture, but, sh..t happens, if so, you can try to make something like below:
final class OrientationController {
static private (set) var allowedOrientation:UIInterfaceOrientationMask = [.all]
// MARK: - Public
class func lockOrientation(_ orientationIdiom: UIInterfaceOrientationMask) {
OrientationController.allowedOrientation = [orientationIdiom]
}
class func forceLockOrientation(_ orientation: UIInterfaceOrientation) {
var mask:UIInterfaceOrientationMask = []
switch orientation {
case .unknown:
mask = [.all]
case .portrait:
mask = [.portrait]
case .portraitUpsideDown:
mask = [.portraitUpsideDown]
case .landscapeLeft:
mask = [.landscapeLeft]
case .landscapeRight:
mask = [.landscapeRight]
}
OrientationController.lockOrientation(mask)
UIDevice.current.setValue(orientation.rawValue, forKey: "orientation")
}
}
Than, in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// do stuff
OrientationController.lockOrientation(.portrait)
return true
}
// MARK: - Orientation
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return OrientationController.allowedOrientation
}
And whenever you want to change orientation do as:
OrientationController.forceLockOrientation(.landscapeRight)
Note: Sometimes, device may not update from such call, so you may need to do as follow
OrientationController.forceLockOrientation(.portrait)
OrientationController.forceLockOrientation(.landscapeRight)
That's all
This is a feedback to comments in Sid Shah's answer, regarding how to disable animations using:
[UIView setAnimationsEnabled:enabled];
Code:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:NO];
[UIView setAnimationsEnabled:NO];
// Stackoverflow #26357162 to force orientation
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:NO];
[UIView setAnimationsEnabled:YES];
}
This should work from iOS 6 on upwards, but I've only tested it on iOS 8. Subclass UINavigationController and override the following methods:
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationLandscapeRight;
}
- (BOOL)shouldAutorotate {
return NO;
}
Or ask the visible view controller
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return self.visibleViewController.preferredInterfaceOrientationForPresentation;
}
- (BOOL)shouldAutorotate {
return self.visibleViewController.shouldAutorotate;
}
and implement the methods there.
If you are using navigationViewController you should create your own superclass for this and override:
- (BOOL)shouldAutorotate {
id currentViewController = self.topViewController;
if ([currentViewController isKindOfClass:[SecondViewController class]])
return NO;
return YES;
}
this will disable rotation in SecondViewController but if you push your SecondViewController when your device is on portrait orientation then your SecondViewController will appear in portrait mode.
Assume that you are using storyboard. You have to create manual segue (How to) and in your "onClick" method:
- (IBAction)onPlayButtonClicked:(UIBarButtonItem *)sender {
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeLeft];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
[self performSegueWithIdentifier:#"PushPlayerViewController" sender:self];
}
This will force landscape orientation before your superclass disable autorotate feature.
On Xcode 8 the methods are converted to properties, so the following works with Swift:
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}
override public var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return UIInterfaceOrientation.portrait
}
override public var shouldAutorotate: Bool {
return true
}
I have tried many solutions, but the one that worked for is the following:
There is no need to edit the info.plist in ios 8 and 9.
- (BOOL) shouldAutorotate {
return NO;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return (UIInterfaceOrientationPortrait | UIInterfaceOrientationPortraitUpsideDown);
}
Possible orientations from the Apple Documentation:
UIInterfaceOrientationUnknown
The orientation of the device cannot be determined.
UIInterfaceOrientationPortrait
The device is in portrait mode, with the device held upright and the
home button on the bottom.
UIInterfaceOrientationPortraitUpsideDown
The device is in portrait mode but upside down, with the device held
upright and the home button at the top.
UIInterfaceOrientationLandscapeLeft
The device is in landscape mode, with the device held upright and the
home button on the left side.
UIInterfaceOrientationLandscapeRight
The device is in landscape mode, with the device held upright and the
home button on the right side.
[iOS9+]
If anyone dragged all the way down here as none of above solutions worked,
and if you present the view you want to change orientation
by segue, you might wanna check this.
Check your segue's presentation type.
Mine was 'over current context'.
When I changed it to Fullscreen, it worked.
Thanks to #GabLeRoux, I found this solution.
This changes only works when combined with solutions above.
The combination of Sids and Koreys answers worked for me.
Extending the Navigation Controller:
extension UINavigationController {
public override func shouldAutorotate() -> Bool {
return visibleViewController.shouldAutorotate()
}
}
Then disabling rotation on the single View
class ViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
}
And rotating to the appropriate orientation before the segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "SomeSegue")
{
let value = UIInterfaceOrientation.Portrait.rawValue;
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
}
The top solution above:
let value = UIInterfaceOrientation.LandscapeLeft.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
didnt'work for me when I called it in viewDidAppear of the presented view controller. However it did work when I called it in preparForSegue in the presenting view controller.
(Sorry, not enough reputation points to comment on that solution, so I had to add it like this)
My requirements are
lock all views in portrait mode
use AVPlayerViewController to play video
When video is playing, if it's a landscape then allow the screen to rotate landscape right and landscape left. If it's a portrait then lock the view in portrait mode only.
First, define supportedInterfaceOrientationsForWindow in AppDelegate.swift
var portrait = true
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if portrait {
return .Portrait
} else {
return .Landscape
}
}
Second, in your main view controller, define following functions
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
print("\(#function)")
return .Portrait
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return .Portrait
}
override func shouldAutorotate() -> Bool {
return false
}
Then, you need to subclass AVPlayerViewController
class MyPlayerViewController: AVPlayerViewController {
var size: CGSize?
var supportedOrientationMask: UIInterfaceOrientationMask?
var preferredOrientation: UIInterfaceOrientation?
override func viewDidLoad() {
super.viewDidLoad()
if let size = size {
if size.width > size.height {
self.supportedOrientationMask =[.LandscapeLeft,.LandscapeRight]
self.preferredOrientation =.LandscapeRight
} else {
self.supportedOrientationMask =.Portrait
self.preferredOrientation =.Portrait
}
}
}
Override these three functions in MyPlayerViewController.swift
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return self.supportedOrientationMask!
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return self.preferredOrientation!
}
Because user might rotate device landscape left or landscape right, we need to set auto rotate to be true
override func shouldAutorotate() -> Bool {
return true
}
Finally, create MyPlayerViewController instance in your view controller and set the property size value.
let playerViewController = MyPlayerViewController()
// Get the thumbnail
let thumbnail = MyAlbumFileManager.sharedManager().getThumbnailFromMyVideoMedia(......)
let size = thumbnail?.size
playerViewController.size = size
Initiate your player with proper videoUrl, then assign your player to playerViewController. Happy coding!
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[UIViewController attemptRotationToDeviceOrientation];
}
There still seems to be some debate about how best to accomplish this task, so I thought I'd share my (working) approach. Add the following code in your UIViewController implementation:
- (void) viewWillAppear:(BOOL)animated
{
[UIViewController attemptRotationToDeviceOrientation];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
{
return (toInterfaceOrientation == UIInterfaceOrientationLandscapeLeft);
}
-(BOOL)shouldAutorotate
{
return NO;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskLandscapeLeft;
}
For this example, you will also need to set your allowed device orientations to 'Landscape Left' in your project settings (or directly in info.plist). Just change the specific orientation you want to force if you want something other than LandscapeLeft.
The key for me was the attemptRotationToDeviceOrientation call in viewWillAppear - without that the view would not properly rotate without physically rotating the device.
According to Korey Hinton's answer
Swift 2.2:
extension UINavigationController {
public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return visibleViewController!.supportedInterfaceOrientations()
}
public override func shouldAutorotate() -> Bool {
return visibleViewController!.shouldAutorotate()
}
}
extension UITabBarController {
public override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if let selected = selectedViewController {
return selected.supportedInterfaceOrientations()
}
return super.supportedInterfaceOrientations()
}
public override func shouldAutorotate() -> Bool {
if let selected = selectedViewController {
return selected.shouldAutorotate()
}
return super.shouldAutorotate()
}
}
Disable Rotation
override func shouldAutorotate() -> Bool {
return false
}
Lock to Specific Orientation
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
I tried a few solutions in here and the important thing to understand is that it's the root view controller that will determine if it will rotate or not.
I created the following objective-c project github.com/GabLeRoux/RotationLockInTabbedViewChild with a working example of a TabbedViewController where one child view is allowed rotating and the other child view is locked in portrait.
It's not perfect but it works and the same idea should work for other kind of root views such as NavigationViewController. :)
According to solution showed by #sid-sha you have to put everything in the viewDidAppear: method, otherwise you will not get the didRotateFromInterfaceOrientation: fired, so something like:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft ||
interfaceOrientation == UIInterfaceOrientationLandscapeRight) {
NSNumber *value = [NSNumber numberWithInt:interfaceOrientation];
[[UIDevice currentDevice] setValue:value forKey:#"orientation"];
}
}
My solution
In AppDelegate:
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if let topController = UIViewController.topMostViewController() {
if topController is XXViewController {
return [.Portrait, .LandscapeLeft]
}
}
return [.Portrait]
}
XXViewController is the ViewController you want to support Landscape mode.
Then Sunny Shah's solution would work in your XXViewController on any iOS version:
let value = UIInterfaceOrientation.LandscapeLeft.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
This is the utility function to find the top most ViewController.
extension UIViewController {
/// Returns the current application's top most view controller.
public class func topMostViewController() -> UIViewController? {
let rootViewController = UIApplication.sharedApplication().windows.first?.rootViewController
return self.topMostViewControllerOfViewController(rootViewController)
}
/// Returns the top most view controller from given view controller's stack.
class func topMostViewControllerOfViewController(viewController: UIViewController?) -> UIViewController? {
// UITabBarController
if let tabBarController = viewController as? UITabBarController,
let selectedViewController = tabBarController.selectedViewController {
return self.topMostViewControllerOfViewController(selectedViewController)
}
// UINavigationController
if let navigationController = viewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
return self.topMostViewControllerOfViewController(visibleViewController)
}
// presented view controller
if let presentedViewController = viewController?.presentedViewController {
return self.topMostViewControllerOfViewController(presentedViewController)
}
// child view controller
for subview in viewController?.view?.subviews ?? [] {
if let childViewController = subview.nextResponder() as? UIViewController {
return self.topMostViewControllerOfViewController(childViewController)
}
}
return viewController
}
}
Use this to lock view controller orientation, tested on IOS 9:
// Lock orientation to landscape right
-(UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskLandscapeRight;
}
-(NSUInteger)navigationControllerSupportedInterfaceOrientations:(UINavigationController *)navigationController {
return UIInterfaceOrientationMaskLandscapeRight;
}
I have the same problem and waste so many time for it. So now I have my solution. My app setting is just support portrait only.However, some screens into my app need have landscape only.I fix it by have a variable isShouldRotate at AppDelegate. And the function at AppDelegate:
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> Int {
if isShouldRotate == true {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
And finally when a ViewControllerA need landscape state. Just do that: before push/present to ViewControllerA assign isShouldRotate to true. Don't forget when pop/dismiss that controller assign isShouldRotate to false at viewWillDisappear.
For me, the top level VC needed to implement the orientation overrides. Using VC's down the stack will have no effect if the top VC is not implementing.
VC-main
|
-> VC 2
|
-> VC 3
Only VC-Main is listened to, essentially in my testing.
It looks like even thou here is so much answers no one was sufficient for me. I wanted to force orientation and then on going back go back to device orientation but [UIViewController attemptRotationToDeviceOrientation]; just did'nt work. What also did complicated whole thing is that I added shouldAutorotate to false based on some answer and could not get desired effects to rotate back correctly in all scenarios.
So this is what I did:
Before pushing of controller in call in his init constructor this:
_userOrientation = UIDevice.currentDevice.orientation;
[UIDevice.currentDevice setValue:#(UIInterfaceOrientationPortrait) forKey:#"orientation"];
[self addNotificationCenterObserver:#selector(rotated:)
name:UIDeviceOrientationDidChangeNotification];
So I save last device orientation and register for orientation change event. Orientation change event is simple:
- (void)rotated:(NSNotification*)notification {
_userOrientation = UIDevice.currentDevice.orientation;
}
And on view dissmising I just force back to any orientation I have as userOreintation:
- (void)onViewDismissing {
super.onViewDismissing;
[UIDevice.currentDevice setValue:#(_userOrientation) forKey:#"orientation"];
[UIViewController attemptRotationToDeviceOrientation];
}
And this has to be there too:
- (BOOL)shouldAutorotate {
return true;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
And also navigation controller has to delegate to shouldAutorotate and supportedInterfaceOrientations, but that most people already have I believe.
PS: Sorry I use some extensions and base classes but names are quite meaningful so concept is understandable, will make even more extensions because it's not too much pretty now.

Resources