143

Since my app got support for all orientation. I would like to lock only portrait mode to specific UIViewController.

e.g. assume it was Tabbed Application and when SignIn View appear modally, I only want that SignIn View to the portrait mode only no matter how the user rotate the device or how the current device orientation will be

Ronan Boiteau
  • 9,608
  • 6
  • 34
  • 56
Thiha Aung
  • 5,036
  • 8
  • 36
  • 79

17 Answers17

367

Things can get quite messy when you have a complicated view hierarchy, like having multiple navigation controllers and/or tab view controllers.

This implementation puts it on the individual view controllers to set when they would like to lock orientations, instead of relying on the App Delegate to find them by iterating through subviews.

Swift 3, 4, 5

In AppDelegate:

/// set orientations you want to be allowed in this property by default
var orientationLock = UIInterfaceOrientationMask.all

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return self.orientationLock
}

In some other global struct or helper class, here I created AppUtility:

struct AppUtility {

    static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
    
        if let delegate = UIApplication.shared.delegate as? AppDelegate {
            delegate.orientationLock = orientation
        }
    }

    /// OPTIONAL Added method to adjust lock and rotate to the desired orientation
    static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation:UIInterfaceOrientation) {
   
        self.lockOrientation(orientation)
    
        UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
        UINavigationController.attemptRotationToDeviceOrientation()
    }

}

Then in the desired ViewController you want to lock orientations:

 override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    
    AppUtility.lockOrientation(.portrait)
    // Or to rotate and lock
    // AppUtility.lockOrientation(.portrait, andRotateTo: .portrait)
    
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    // Don't forget to reset when view is being removed
    AppUtility.lockOrientation(.all)
}

If iPad or Universal App

Make sure that "Requires full screen" is checked in Target Settings -> General -> Deployment Info. supportedInterfaceOrientationsFor delegate will not get called if that is not checked. enter image description here

bmjohns
  • 6,344
  • 1
  • 33
  • 39
  • Looks like it's code control...let me check it,seems like good answer. – Thiha Aung Jan 24 '17 at 10:11
  • 20
    After a lot of insanely research, this is the only answer I found that works for me. Swift 3 - Xcode 8.2.1 – Xavier Garcia Rubio Feb 01 '17 at 08:52
  • Another great solution for Swift 3. I will updated this and refer to you. – Thiha Aung Mar 29 '17 at 19:24
  • @ThihaAung, when I uncomment the AppUtility.lockOrientation(.portrait, andRotateTo: .landscape) . I got error. UIInterfaceorientation has no member 'landscape.' The reason why I change landscape is above code does not effect in my app and I would like to be is when user click on movie play view controller, then goes to another page and immediately rotates to Landscape. I would like to do this. Could you help me bro? – May Phyu Apr 08 '17 at 08:04
  • @bmjohns, when I uncomment the AppUtility.lockOrientation(.portrait, andRotateTo: .landscape) . I got error. UIInterfaceorientation has no member 'landscape.' The reason why I change landscape is above code does not effect in my app and I would like to be is when user click on movie play view controller, then goes to another page and immediately rotates to Landscape. I would like to do this. Could you help me bro? – May Phyu Apr 08 '17 at 08:18
  • @MayPhyu Check out the properties of UIInterfaceOrientation, you need to use landscapeLeft or .landscapeRight. https://developer.apple.com/reference/uikit/uiinterfaceorientation – bmjohns Apr 08 '17 at 14:00
  • @MayPhyu, your requirement and mine are the same. Please post a question to get clear more detail for us. Mine was setting the all the app UI to portrait. But, when I move to video view controller, I want it enable portrait,landscape left and right. So, user can see the video as they want to. – Thiha Aung Apr 08 '17 at 17:54
  • @bmjohns Hi, I am trying to use de Swift 3.0 answer but it doesn't work. If I have my phone on landscape, when I reach the ViewController where I want to force Portrait it doesn't change it – Carlos Luis Urbina Apr 11 '17 at 14:24
  • @CarlosLuisUrbina Make sure that you use `AppUtility.lockOrientation(.portrait, andRotateTo: .portrait)` in `viewDidAppear`. There are two functions in my answer, one is to just lock orientations, and the other is to rotate and lock. If you always want the device to rotate if not in desired position then always use `lockOrientation(andRotateTo:)` function – bmjohns Apr 11 '17 at 15:23
  • This solution works for me. But I'm concerned Apple will not approve an App using this code. Has anyone used this solution and had their App rejected or approved by Apple? – pbm Apr 18 '17 at 01:12
  • 1
    @pbm Several of the applications at the company I work at lock orientations at specific parts throughout the app, and have all been approved to the app store multiple times, over many years. That said, though, I can't guarantee Apple will not have a problem with this down the road. But so far no issues, and I am fairly certain many apps do this in some form or another. – bmjohns Apr 18 '17 at 16:19
  • @NileshPol I have it working in an iPad and iPhone application. You might want to make sure you have all the orientations supported in the plist, also are the methods being called like you expect when debugging? – bmjohns May 06 '17 at 14:27
  • This is the only solution that worked for me. +1 Swift 3 - Xcode up to date – Jorge Irún May 08 '17 at 13:37
  • 3
    If checking `Requires full screen`, that will prevent the app available to Slide Over and Split View. See [Adopting Multitasking Enhancements on iPad](https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/AdoptingMultitaskingOniPad/index.html#//apple_ref/doc/uid/TP40015145-CH3-SW1) by Apple. I have an [answer](https://stackoverflow.com/a/48120684/3731039) doesn't require enable `Requires full screen` – John Pang Jan 05 '18 at 20:26
  • @JohnPang Yes it does prevent it. But if you uncheck `Requires full screen` then all orientation delegates will not fire in an iPad application (iPhone they still will). That includes `shouldAutorotate`, `preferredInterfaceOrientation`, and `supportedInterfaceOrientations`. Your answer still uses `supportedInterfaceOrientationsFor`. Did you only test in iPhone? – bmjohns Jan 05 '18 at 21:02
  • @bmjohns I didn't notice that `application(_:supportedInterfaceOrientationsFor)` didn't get fired on iPad until you mentioned. My app runs in portrait on iPhone, except an image viewer. Due to Smart Cover, I believe many users will be using iPad in landscape. I found the way to get it fired on iPad without `Requires full screen`. In the Info.plist, reduce both `Supported interface orientations` and `Supported interface orientations (iPad)` down to just one item: `Portrait (bottom home button)`. However, the app won't appears in Slide Over anymore. Slide Over is just portrait, isn't it? – John Pang Jan 06 '18 at 21:53
  • I found an issue where if you select all the orientations in the Deployment Info and start the app while the app is in landscape, it will come out as landscape regardless of locking the screen to portrait. A fix is to just select portrait in the deployment info. – Ameya Vichare Apr 03 '18 at 06:16
  • 2
    Its not working, second viewController layout not updated its showing in portrait only. – The iCoder Apr 05 '18 at 15:27
  • Ameya Vichare. Thats not a solution. – Jeff Apr 28 '18 at 19:58
  • Works seamlessly. Thank you so much! Had to change a bit of code here and there to smoothly handle the transition from one `ViewContoller` to another when in different orientations. Except for that. WOW! – Paddler Apr 05 '19 at 09:29
  • **This works well** for both iPhone and iPad, but in some cases you should move the code from `viewWillAppear` to `viewDidAppear` (especially for SpriteKit enviroments..) to permit the correct layout drawing => rotate and use `AppUtility.lockOrientation(.portrait, andRotateTo: .portrait) ` – Alessandro Ornano Apr 24 '19 at 11:05
  • Anyone with a Swift 5 and iOS 13 version? This one isn't working anymore on iOS 13 for me :( – Dam Nov 07 '19 at 15:55
  • neat and well explained solution! – Mihir Oza Jul 28 '20 at 17:55
  • Working on latest iOS 14.6 :) Thanks! – Bhavesh Bansal Aug 25 '21 at 07:40
  • Ensure interface orientation is _not_ specified in your Info.plist, otherwise the delegate method will not be called. – David James Feb 01 '22 at 18:31
  • 3
    Does not work with iOS 16 anymore. Throws following error: `BUG IN CLIENT OF UIKIT: Setting UIDevice.orientation is not supported. Please use UIWindowScene.requestGeometryUpdate(_:)`. – birkoof Oct 16 '22 at 07:35
43

Swift 4/5

Project -> General enter image description here AppDelegate

var orientationLock = UIInterfaceOrientationMask.all
    
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    return self.orientationLock
}
struct AppUtility {
    static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
        if let delegate = UIApplication.shared.delegate as? AppDelegate {
            delegate.orientationLock = orientation
        }
    }
        
    static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation:UIInterfaceOrientation) {
        self.lockOrientation(orientation)
        UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
    }
}

Your ViewController Add the following line if you need only portrait orientation. you have to apply this to all ViewController need to display portrait mode.

override func viewWillAppear(_ animated: Bool) {
    AppDelegate.AppUtility.lockOrientation(UIInterfaceOrientationMask.portrait, andRotateTo: UIInterfaceOrientation.portrait)
}

and that will make screen orientation for others Viewcontroller according to device physical orientation.

override func viewWillDisappear(_ animated: Bool) {
    AppDelegate.AppUtility.lockOrientation(UIInterfaceOrientationMask.all)
}
Nahid Raihan
  • 957
  • 1
  • 10
  • 20
  • 1
    For Swift 4 this is definitely the top answer. It is exactly what I needed as it mimics the behavior of the iOS camera app. – nocdib Jul 27 '18 at 03:50
  • @Nahid Do we replace the original `func application` within app delegate with this function `func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return self.orientationLock ? ` – Moondra Aug 10 '18 at 03:36
  • After hour or two of looking for solution and some attempts, this is the perfectly working solution. It set the orientation of view, forces it when view is loaded, unlock when going to another view, it works in tab navigation and the code is simple and clean. Thank you @Nahid – Radek Jan 02 '20 at 22:15
19

Swift 3 & 4

Set the supportedInterfaceOrientations property of specific UIViewControllers like this:

class MyViewController: UIViewController {

    var orientations = UIInterfaceOrientationMask.portrait //or what orientation you want
    override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
    get { return self.orientations }
    set { self.orientations = newValue }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    //...
}

UPDATE

This solution only works when your viewController is not embedded in UINavigationController, because the orientation inherits from parent viewController.
For this case, you can create a subclass of UINavigationViewController and set these properties on it.

Arash Etemad
  • 1,827
  • 1
  • 13
  • 29
  • Thanks. This works well for me because I only want to restrict a specific View to Portrait. Not the entire app. – user3204765 Apr 06 '19 at 16:21
  • All I need is to control specific view controllers to lock orientation regardless of devise orientation and this worked great! – Ryan B. Jan 09 '20 at 05:37
17

For a new version of Swift try this

override var shouldAutorotate: Bool {
    return false
}

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.portrait
}

override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
    return UIInterfaceOrientation.portrait
}
  • It is exactly what I was needed ! For iPad controller all orientation options are valid and for iPhone controller only portrait mode by writing in it only these three overrides - Perfect. – VYT May 06 '20 at 18:38
11

Add this code to force portrait and lock it:

override func viewDidLoad() {
    super.viewDidLoad()

    // Force the device in portrait mode when the view controller gets loaded
    UIDevice.currentDevice().setValue(UIInterfaceOrientation.Portrait.rawValue, forKey: "orientation") 
}

override func shouldAutorotate() -> Bool {
    // Lock autorotate
    return false
}

override func supportedInterfaceOrientations() -> Int {

    // Only allow Portrait
    return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}

override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {

    // Only allow Portrait
    return UIInterfaceOrientation.Portrait
}

In your AppDelegate - set supportedInterfaceOrientationsForWindow to whatever orientations you want the entire application to support:

func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
    return UIInterfaceOrientationMask.All
} 
Valentin
  • 3,272
  • 1
  • 14
  • 22
  • Thanks Valentin,I really appreciated your helps.But,you code didn't work out.And I have solved now.I also answered to my question. – Thiha Aung Mar 10 '15 at 03:24
8

This is a generic solution for your problem and others related.

1. Create auxiliar class UIHelper and put on the following methods:

    /**This method returns top view controller in application  */
    class func topViewController() -> UIViewController?
    {
        let helper = UIHelper()
        return helper.topViewControllerWithRootViewController(rootViewController: UIApplication.shared.keyWindow?.rootViewController)
    }

    /**This is a recursive method to select the top View Controller in a app, either with TabBarController or not */
    private func topViewControllerWithRootViewController(rootViewController:UIViewController?) -> UIViewController?
    {
        if(rootViewController != nil)
        {
            // UITabBarController
            if let tabBarController = rootViewController as? UITabBarController,
                let selectedViewController = tabBarController.selectedViewController {
                return self.topViewControllerWithRootViewController(rootViewController: selectedViewController)
            }

            // UINavigationController
            if let navigationController = rootViewController as? UINavigationController ,let visibleViewController = navigationController.visibleViewController {
                return self.topViewControllerWithRootViewController(rootViewController: visibleViewController)
            }

            if ((rootViewController!.presentedViewController) != nil) {
                let presentedViewController = rootViewController!.presentedViewController;
                return self.topViewControllerWithRootViewController(rootViewController: presentedViewController!);
            }else
            {
                return rootViewController
            }
        }

        return nil
    }

2. Create a Protocol with your desire behavior, for your specific case will be portrait.

protocol orientationIsOnlyPortrait {}

Nota: If you want, add it in the top of UIHelper Class.

3. Extend your View Controller

In your case:

class Any_ViewController: UIViewController,orientationIsOnlyPortrait {

   ....

}

4. In app delegate class add this method:

 func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        let presentedViewController = UIHelper.topViewController()
        if presentedViewController is orientationIsOnlyPortrait {
            return .portrait
        }
        return .all
    }

Final Notes:

  • If you that more class are in portrait mode, just extend that protocol.
  • If you want others behaviors from view controllers, create other protocols and follow the same structure.
  • This example solves the problem with orientations changes after push view controllers
Ariel Antonio Fundora
  • 1,330
  • 15
  • 20
6

A bunch of great answers in this thread, but none quite matched my needs. I have a tabbed app with navigation controllers in each tab, and one view needed to rotate, while the others needed to be locked in portrait. The navigation controller wasn't resizing it's subviews properly, for some reason. Found a solution (in Swift 3) by combining with this answer, and the layout issues disappeared. Create the struct as suggest by @bmjohns:

import UIKit

struct OrientationLock {

    static func lock(to orientation: UIInterfaceOrientationMask) {
        if let delegate = UIApplication.shared.delegate as? AppDelegate {
            delegate.orientationLock = orientation
        }
    }

    static func lock(to orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation) {
        self.lock(to: orientation)
        UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
    }
} 

Then subclass UITabBarController:

    import UIKit

class TabBarController: UITabBarController, UITabBarControllerDelegate {
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.delegate = self
    }

    func tabBarControllerSupportedInterfaceOrientations(_ tabBarController: UITabBarController) -> UIInterfaceOrientationMask {
        if tabBarController.selectedViewController is MyViewControllerNotInANavigationControllerThatShouldRotate {
            return .allButUpsideDown
        } else if let navController = tabBarController.selectedViewController as? UINavigationController, navController.topViewController is MyViewControllerInANavControllerThatShouldRotate {
            return .allButUpsideDown
        } else {
            //Lock view that should not be able to rotate
            return .portrait
        }
    }

    func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
        if viewController is MyViewControllerNotInANavigationControllerThatShouldRotate {
            OrientationLock.lock(to: .allButUpsideDown)
        } else if let navController = viewController as? UINavigationController, navController.topViewController is MyViewControllerInANavigationControllerThatShouldRotate {
            OrientationLock.lock(to: .allButUpsideDown)
        } else {
            //Lock orientation and rotate to desired orientation
            OrientationLock.lock(to: .portrait, andRotateTo: .portrait)
        }
        return true
    }
}

Don't forget to change the class of the TabBarController in the storyboard to the newly created subclass.

Community
  • 1
  • 1
russdub
  • 245
  • 4
  • 9
4

Here is a simple way that works for me with Swift 4.2 (iOS 12.2), put this in a UIViewController for which you want to disable shouldAutorotate:

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return .portrait
}

The .portrait part tells it in which orientation(s) to remain, you can change this as you like. Choices are: .portrait, .all, .allButUpsideDown, .landscape, .landscapeLeft, .landscapeRight, .portraitUpsideDown.

GregT
  • 2,194
  • 2
  • 19
  • 15
4

For iOS 16 the accepted answer wasn't working. but was able to get it working. Just replace

UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")

with this,

if #available(iOS 16.0, *) {
        guard
            let rootViewController = UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.rootViewController,
            let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
        else { return }
        rootViewController.setNeedsUpdateOfSupportedInterfaceOrientations()
        windowScene.requestGeometryUpdate(.iOS(
            interfaceOrientations: windowScene.interfaceOrientation.isLandscape
                ? .portrait
                : .landscapeRight
        ))
    } else {
        UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
    }
payakan
  • 41
  • 1
3

Best Solution for lock and change orientation on portrait and landscape:

Watch this video on YouTube:

https://m.youtube.com/watch?v=4vRrHdBowyo

This tutorial is best and simple.

or use below code:

See this picture

// 1- in second viewcontroller we set landscapeleft and in first viewcontroller we set portrat:

// 2- if you use NavigationController, you should add extension

import UIKit

class SecondViewController: UIViewController {


    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
    }

    override open var shouldAutorotate: Bool {
        return false
    }

    override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .landscapeLeft
    }

    override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
        return .landscapeLeft
    }
    
    

    override func viewDidLoad() {
        super.viewDidLoad()
    }

//write The rest of your code in here


}

//if you use NavigationController, you should add this extension

extension UINavigationController {
    override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return topViewController?.supportedInterfaceOrientations ?? .allButUpsideDown
    
    }
}
OliaPh
  • 29
  • 3
2

To set Landscape orientation to all view of your app & allow only one view to All orientations (to be able to add camera roll for example):

In AppDelegate.swift:

var adaptOrientation = false

In: didFinishLaunchingWithOptions

NSNotificationCenter.defaultCenter().addObserver(self, selector: "adaptOrientationAction:", name:"adaptOrientationAction", object: nil)

Elsewhere in AppDelegate.swift:

func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> Int {
    return checkOrientation(self.window?.rootViewController)
}

func checkOrientation(viewController:UIViewController?)-> Int{
    if (adaptOrientation == false){
        return Int(UIInterfaceOrientationMask.Landscape.rawValue)
    }else {
        return Int(UIInterfaceOrientationMask.All.rawValue)
    }
}

func adaptOrientationAction(notification: NSNotification){
    if adaptOrientation == false {
        adaptOrientation = true
    }else {
        adaptOrientation = false
    }
}

Then in the view that segue to the one you want to be able to have All orientations:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
    if (segue.identifier == "YOURSEGUE") {
        NSNotificationCenter.defaultCenter().postNotificationName("adaptOrientationAction", object: nil)
    }
}

override func viewWillAppear(animated: Bool) {
    if adaptOrientation == true {
        NSNotificationCenter.defaultCenter().postNotificationName("adaptOrientationAction", object: nil)
    }
}

Last thing is to tick Device orientation: - Portrait - Landscape Left - Landscape Right

iOS Flow
  • 69
  • 1
  • 8
2

bmjohns -> You are my life saviour. That is the only working solution (With the AppUtility struct)

I've created this class:

class Helper{
    struct AppUtility {

        static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {

            if let delegate = UIApplication.shared.delegate as? AppDelegate {
                delegate.orientationLock = orientation
            }
        }

        /// OPTIONAL Added method to adjust lock and rotate to the desired orientation
        static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation:UIInterfaceOrientation) {

            self.lockOrientation(orientation)

            UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
        }

    }
}

and followed your instructions, and everything works perfectly for Swift 3 -> xcode version 8.2.1

2

Create new extension with

import UIKit

extension UINavigationController {
    override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }
}

extension UITabBarController {
    override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }
}
2

Actual tested Solution for this.In my example I need my whole app should be in portrait mode, but only one screen's orientation should be in landscape mode. Make a Portrait orientation for app , by check only portrait mode

Code in AppDelegate as above answers described.

var orientationLock = UIInterfaceOrientationMask.all

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask 
{
  return self.orientationLock
}
struct AppUtility {  

static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
    if let delegate = UIApplication.shared.delegate as? AppDelegate {
        delegate.orientationLock = orientation
    }
}
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation:UIInterfaceOrientation) {
self.lockOrientation(orientation)     
UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
}  
}

Then write down this code before your landscape orientation viewcontroller will be presented/push.

override func viewWillAppear(_ animated: Bool) {  
super.viewWillAppear(animated)
AppDelegate.AppUtility.lockOrientation(UIInterfaceOrientationMask.portrait, andRotateTo: UIInterfaceOrientation.portrait)
}  

Then write down this code in actual viewcontroller(For landscape view)

override func viewWillAppear(_ animated: Bool) {  
super.viewWillAppear(animated)
AppDelegate.AppUtility.lockOrientation(UIInterfaceOrientationMask.landscape, andRotateTo: UIInterfaceOrientation.landscape)
}  
Gurpreet Singh
  • 803
  • 9
  • 16
1

As of iOS 10 and 11, iPad supports Slide Over and Split View. To enable an app in Slide Over and Split View, Requires full screen must be unchecked. That means the accepted answer cannot be used if the app wants to support Slide Over and Split View. See more from Apple's Adopting Multitasking Enhancements on iPad here.

I have a solution that allows (1) unchecking Requires full screen, (2) just one function to be implemented in appDelegate (especially if you don't want to / can't modify the target view controllers), and (3) avoid recursive calls. No need of helper class nor extensions.

appDelegate.swift (Swift 4)

func application(_ application: UIApplication,
                 supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    // Search for the visible view controller
    var vc = window?.rootViewController
    // Dig through tab bar and navigation, regardless their order 
    while (vc is UITabBarController) || (vc is UINavigationController) {
        if let c = vc as? UINavigationController {
            vc = c.topViewController
        } else if let c = vc as? UITabBarController {
            vc = c.selectedViewController
        }
    }
    // Look for model view controller
    while (vc?.presentedViewController) != nil {
        vc = vc!.presentedViewController
    }
    print("vc = " + (vc != nil ? String(describing: type(of: vc!)) : "nil"))
    // Final check if it's our target class.  Also make sure it isn't exiting.
    // Otherwise, system will mistakenly rotate the presentingViewController.
    if (vc is TargetViewController) && !(vc!.isBeingDismissed) {
        return [.portrait]
    }
    return [.all]
}

Edit

@bmjohns pointed out that this function is not called on iPad. I verified and yes it was not called. So, I did a bit more testing and found out some facts:

  1. I unchecked Requires full screen because I want to enable Slide Over and Slide View on iPad. That requires the app to support all 4 orientation for iPad, in Info.plist: Supported interface orientations (iPad).

My app works same way as Facebook: on iPhone, most of the time it is locked to portrait. When viewing image in full screen, allows users to rotate landscape for better view. On iPad, users can rotate to any orientation in any view controllers. So, the app looks nice when iPad is stand on Smart Cover (landscape left).

  1. For iPad to call application(_:supportedInterfaceOrientationsFor), in Info.plist, only keep portrait for iPad. The app will lose Slide Over + Split View ability. But you can lock or unlock the orientation for any view controller, in just one place and no need to modify ViewController class.

  2. Finally, this function get called on view controller's life cycle, when view is displayed/removed. If your app need to lock/unlock/change orientation in other time, it might not work

John Pang
  • 2,403
  • 25
  • 25
  • Is it working for iPhone ? I want show one of my viewController in landscape only. – The iCoder Apr 05 '18 at 15:30
  • Yes. work for iPhone. I want one of my view controller in both portrait and landscape, and return to portrait after dismissing it. – John Pang Apr 07 '18 at 09:54
1

I experimented a little bit and I managed to find clean solution for this problem. The approach is based on the view tagging via view->tag

In the target ViewController just assign the tag to the root view like in the following code example:

class MyViewController: BaseViewController {

  // declare unique view tag identifier
  static let ViewTag = 2105981;

  override func viewDidLoad()
  {
    super.viewDidLoad();
    // assign the value to the current root view
    self.view.tag = MyViewController.ViewTag;
  }

And finally in the AppDelegate.swift check if the currently shown view is the one we tagged:

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
{
    if (window?.viewWithTag(DesignerController.ViewTag)) != nil {
        return .portrait;
    }
    return .all;
}

This approach has been tested with my simulator and seems it works fine.

Note: the marked view will be also found if current MVC is overlapped with some child ViewController in navigation stack.

Vlada
  • 141
  • 1
  • 7
-3

Thanks to @bmjohn's answer above. Here is a working Xamarin / C# version of the that answer's code, to save others the time of transcription:

AppDelegate.cs

 public UIInterfaceOrientationMask OrientationLock = UIInterfaceOrientationMask.All;
 public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
 {
     return this.OrientationLock;
 }

Static OrientationUtility.cs class:

public static class OrientationUtility
{
    public static void LockOrientation(UIInterfaceOrientationMask orientation)
    {
        var appdelegate = (AppDelegate) UIApplication.SharedApplication.Delegate;
        if(appdelegate != null)
        {
            appdelegate.OrientationLock = orientation;
        }
    }

    public static void LockOrientation(UIInterfaceOrientationMask orientation, UIInterfaceOrientation RotateToOrientation)
    {
        LockOrientation(orientation);

        UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)RotateToOrientation), new NSString("orientation"));
    }
}

View Controller:

    public override void ViewDidAppear(bool animated)
    {
       base.ViewWillAppear(animated);
       OrientationUtility.LockOrientation(UIInterfaceOrientationMask.Portrait, UIInterfaceOrientation.Portrait);
    }

    public override void ViewWillDisappear(bool animated)
    {
        base.ViewWillDisappear(animated);
        OrientationUtility.LockOrientation(UIInterfaceOrientationMask.All);
    }
ccs_dev
  • 13
  • 4
  • I don't understand how this answer is relevant? That person asked in swift and you have given in Xamarin. – Mihir Oza Jul 28 '20 at 17:32