21

I am trying to rotate one view while all other views (5) are fixed to portrait. The reason is that in that one view I want the user to watch pictures which he saved before. I guess this is possible but so far I couldn't figure out how to achieve that. Can anyone help or give me a hint? I am programming that in Swift running on iOS8

Armin Scheithauer
  • 601
  • 1
  • 7
  • 17
  • When you say "rotate" do you mean you want the view to be able to rotate between landscape and portrait or that you just want it to be locked in landscape while the other views are in portrait? – Lyndsey Scott Jan 30 '15 at 19:38
  • Yes you are right, as some pictures ore landscape and others are portrait I want the view rotate. But only in this one view, all other views need to be fixed to portrait. Now I hardcoded all views to portrait but want to change that one view. – Armin Scheithauer Jan 30 '15 at 19:46

14 Answers14

43

I'd recommend using supportedInterfaceOrientationsForWindow in your appDelegate to allow rotation only in that specific view controller, ex:

Swift 4/Swift 5

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {

    // Make sure the root controller has been set
    // (won't initially be set when the app is launched)
    if let navigationController = window?.rootViewController as? UINavigationController {

        // If the visible view controller is the
        // view controller you'd like to rotate, allow
        // that window to support all orientations
        if navigationController.visibleViewController is SpecificViewController {
            return UIInterfaceOrientationMask.all
        } 

        // Else only allow the window to support portrait orientation
        else {
            return UIInterfaceOrientationMask.portrait
        }
    }

    // If the root view controller hasn't been set yet, just
    // return anything
    return UIInterfaceOrientationMask.portrait
}

Note that if that SpecificViewController is in landscape before going to a portrait screen, the other view will still open in landscape. To circumvent this, I'd recommend disallowing transitions while that view is in landscape.


Swift 3

func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> Int {

    // Make sure the root controller has been set
    // (won't initially be set when the app is launched)
    if let navigationController = self.window?.rootViewController as? UINavigationController {

        // If the visible view controller is the
        // view controller you'd like to rotate, allow
        // that window to support all orientations
        if navigationController.visibleViewController is SpecificViewController  {
            return Int(UIInterfaceOrientationMask.All.rawValue)
        }

        // Else only allow the window to support portrait orientation
        else {
            return Int(UIInterfaceOrientationMask.Portrait.rawValue)
        }
    }

    // If the root view controller hasn't been set yet, just
    // return anything
    return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
zs2020
  • 53,766
  • 29
  • 154
  • 219
Lyndsey Scott
  • 37,080
  • 10
  • 92
  • 128
  • @ArminScheithauer No problem :) And to be honest, I'm not convinced Christian's solution will work in all situation's because I'm having trouble producing the desired results when testing his code... – Lyndsey Scott Jan 30 '15 at 20:44
  • @ArminScheithauer To get Christian's answer to work you have to subclass the UINavigationViewController and use UInterfaceOrientationMask instead of UInterfaceOrientation among other things... – Lyndsey Scott Jan 30 '15 at 21:01
  • @LyndseyScott what does that mean "disallowing transitions while that view is in landscape." Can you explain it in detail. Thanks – noob Oct 02 '17 at 08:19
14

You can also do it in a protocol oriented way. Just create the protocol

protocol CanRotate {

}

Add the the same 2 methods in the AppDelegate in a more "swifty" way

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    if topViewController(in: window?.rootViewController) is CanRotate {
        return .allButUpsideDown
    } else {
        return .portrait
    }
}


func topViewController(in rootViewController: UIViewController?) -> UIViewController? {
    guard let rootViewController = rootViewController else {
        return nil
    }

    if let tabBarController = rootViewController as? UITabBarController {
        return topViewController(in: tabBarController.selectedViewController)
    } else if let navigationController = rootViewController as? UINavigationController {
        return topViewController(in: navigationController.visibleViewController)
    } else if let presentedViewController = rootViewController.presentedViewController {
        return topViewController(in: presentedViewController)
    }
    return rootViewController
}

And in every ViewController that you want a different behaviour, just add the protocol name in the definition of the class.

class ViewController: UIViewController, CanRotate {}

If you want any particular combination, they you can add to the protocol a variable to override

protocol CanRotate {
    var supportedInterfaceOrientations: UIInterfaceOrientationMask
}
Cristian Pena
  • 2,139
  • 1
  • 19
  • 31
12

Sometimes when you're using a custom navigation flow (that may get really complex) the above-mentioned solutions may not always work. Besides, if you have several ViewControllers that need support for multiple orientations it may get quite tedious.

Here's a rather quick solution I found. Define a class OrientationManager and use it to update supported orientations in AppDelegate:

class OrientationManager {
    static var landscapeSupported: Bool = false
}

Then in AppDelegate put the orientations you want for that specific case:

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if OrientationManager.landscapeSupported {
            return .allButUpsideDown
        }
        return .portrait
    }

Then in the ViewControllers that you want to have multiple navigations update the OrientationManager:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    OrientationManager.landscapeSupported = true
}

Also, don't forget to update it once again when you'll be exiting this ViewController:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    OrientationManager.landscapeSupported = false
    //The code below will automatically rotate your device's orientation when you exit this ViewController
    let orientationValue = UIInterfaceOrientation.portrait.rawValue
    UIDevice.current.setValue(orientationValue, forKey: "orientation")
}

Hope this helps!

Update:

You may just want to add a static func to your Orientation Support Manager class:

    static func setOrientation(_ orientation: UIInterfaceOrientation) {
        let orientationValue = orientation.rawValue
        UIDevice.current.setValue(orientationValue, forKey: "orientation")
        landscapeSupported = orientation.isLandscape
    }

Then you can call this function whenever you need to set the orientation back to portrait. That will also update the static landscapeSupported value:

OSM.setOrientation(.portrait)
Nikita Alexander
  • 527
  • 6
  • 11
9

This is for Swift 4 and Swift 5. You can use the follow code in your AppDelegate.swift :

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    guard let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController),
     (rootViewController.responds(to: Selector(("canRotate")))) else {
        // Only allow portrait (standard behaviour)
        return .portrait;
    }
    // Unlock landscape view orientations for this view controller
    return .allButUpsideDown;
}

private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
    guard rootViewController != nil else { return nil }
    
    guard !(rootViewController.isKind(of: (UITabBarController).self)) else{
        return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
    }
    guard !(rootViewController.isKind(of:(UINavigationController).self)) else{
        return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
    }
    guard !(rootViewController.presentedViewController != nil) else {
        return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
    }
    return rootViewController
}

You can then make a custom UIViewController rotate by overriding shouldAutorotate

gandhi Mena
  • 2,115
  • 1
  • 19
  • 20
  • I also added to ```viewWillDisappear``` ```if (self.isMovingFromParent) { UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation") }``` so, the presenting vc doesn't rotate on pop. – dmyma Mar 08 '19 at 18:22
  • I've made all of these changes but nothing is changing for me, any other suggestions? – Sara Mar 04 '20 at 20:25
8

With everyone's ideas I wrote the most elegant way to do it I think.

Result:

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return (UIApplication.getTopViewController() as? Rotatable == nil) ? .portrait : .allButUpsideDown
    }

Add this extension to your project which will always be useful not only for this:

extension UIApplication {

    class func getTopViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let nav = base as? UINavigationController {
            return getTopViewController(base: nav.visibleViewController)
        }

        if let tab = base as? UITabBarController {
            if let selected = tab.selectedViewController {
                return getTopViewController(base: selected)
            }
        }

        if let presented = base?.presentedViewController {
            return getTopViewController(base: presented)
        }

        return base
    }

}

Create the protocol:

protocol Rotatable {}

And implement it:

class ViewController: UIViewController, Rotatable {
}
Mickael Belhassen
  • 2,970
  • 1
  • 25
  • 46
3

Use the shouldAutorotate and the supportedInterfaceOrientations method in the ViewController you want to display in landscape and portrait mode:

This method should override the storyboard-settings.

override func shouldAutorotate() -> Bool {
    return true
}

override func supportedInterfaceOrientations() -> Int {
    return UIInterfaceOrientation.Portrait.rawValue | UIInterfaceOrientation.LandscapeLeft.rawValue | UIInterfaceOrientation.LandscapeRight.rawValue
}
Christian
  • 22,585
  • 9
  • 80
  • 106
  • 2
    Hey Christian, this answer only seems to work if you subclass the `UINavigationController` and return `self.topViewController.supportedInterfaceOrientations()` and `self.topViewController.shouldAutorotate()` for the `supportedInterfaceOrientations()` and `shouldAutorotate()` functions respectively. – Lyndsey Scott Jan 30 '15 at 21:03
2

I just faced a very similar problem where I wanted to present a video player in portrait and landscape mode whereas the rest of the app is portrait only. My main problem was that when I dismissed the video vc in landscape mode the presenting vc was only briefly in landscape mode.
As pointed out in the comment to @Lyndsey Scott's answer this can be circumvented by disallowing transitions while in landscape mode, but by combining this and this I've found a better and more generic solution (IMO). This solution allows rotation in all vc where you put canRotate(){} and doesn't rotate the presenting vc.

Swift 3:
In AppDelegate.swift:

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
        if (rootViewController.responds(to: Selector(("canRotate")))) {
            // Unlock landscape view orientations for this view controller if it is not currently being dismissed
            if !rootViewController.isBeingDismissed{
                return .allButUpsideDown
            }
        }
    }

    // Only allow portrait (standard behaviour)
    return .portrait
}

private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
    if (rootViewController == nil) {
        return nil
    }
    if (rootViewController.isKind(of: UITabBarController.self)) {
        return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
    } else if (rootViewController.isKind(of: UINavigationController.self)) {
        return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
    } else if (rootViewController.presentedViewController != nil) {
        return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
    }
    return rootViewController
}

In each view controller where rotation should be allowed:

func canRotate(){}
Community
  • 1
  • 1
kuhr
  • 582
  • 8
  • 18
2

Swift 5 using Marker protocol

Combined version of several answers here, done in what I think is a more readable/elegant implementation. (Derived from earlier answers here, not original work by me!)

protocol RotatableViewController {
    // No content necessary, marker protocol
}

class MyViewController: UIViewController, RotatableViewController {
    // normal content... nothing more required
}

extension AppDelegate {

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        guard
            let rootVc = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController),
            rootVc.isBeingDismissed == false,
            let _ = rootVc as? RotatableViewController
        else {
            return .portrait  // Some condition not met, so default answer for app
        }
        // Conditions met, is rotatable:
        return .allButUpsideDown
    }


    private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
        if (rootViewController == nil) {
            return nil
        }
        if (rootViewController.isKind(of: UITabBarController.self)) {
            return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
        }
        else if (rootViewController.isKind(of: UINavigationController.self)) {
            return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
        }
        else if (rootViewController.presentedViewController != nil) {
            return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
        }
        return rootViewController
    }
}
Bill Patterson
  • 2,495
  • 1
  • 19
  • 20
  • 2
    If MyRotatbleVC is presented and when it's dismissed in Landscape mode, the previous VC will in Landscape mode, instead of remaining Portrait. – Tieda Wei Mar 25 '20 at 23:52
1

Swift 3: Add code to AppDelegate.swift

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if let rootViewController = self.topViewControllerWithRootViewController(rootViewController: window?.rootViewController) {
            if (rootViewController.responds(to: Selector(("canRotate")))) {
                // Unlock landscape view orientations for this view controller
                return .allButUpsideDown;
            }
        }

        // Only allow portrait (standard behaviour)
        return .portrait;
    }

    private func topViewControllerWithRootViewController(rootViewController: UIViewController!) -> UIViewController? {
        if (rootViewController == nil) { return nil }
        if (rootViewController.isKind(of: (UITabBarController).self)) {
            return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UITabBarController).selectedViewController)
        } else if (rootViewController.isKind(of:(UINavigationController).self)) {
            return topViewControllerWithRootViewController(rootViewController: (rootViewController as! UINavigationController).visibleViewController)
        } else if (rootViewController.presentedViewController != nil) {
            return topViewControllerWithRootViewController(rootViewController: rootViewController.presentedViewController)
        }
        return rootViewController
    }

Then :

import UIKit

class ViewController: UIViewController {

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


    override func viewWillDisappear(_ animated : Bool) {
        super.viewWillDisappear(animated)

        if (self.isMovingFromParentViewController) {
            UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
        }
    }

    func canRotate() -> Void {}

}

http://www.jairobjunior.com/blog/2016/03/05/how-to-rotate-only-one-view-controller-to-landscape-in-ios-slash-swift/

Mohammad Razipour
  • 3,643
  • 3
  • 29
  • 49
1

SWIFT 4

For UITabBarController can we use this line of code in AppDelegate.swift.

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    if let tabBarController = window?.rootViewController as? UITabBarController {
        if let tabBarViewControllers = tabBarController.viewControllers {
            if let projectsNavigationController = tabBarViewControllers[1] as? UINavigationController {
                if projectsNavigationController.visibleViewController is PickerViewController //use here your own ViewController class name {
                    return .all
                }
            }
        }
    }
    return .portrait
}
PiterPan
  • 1,760
  • 2
  • 22
  • 43
1

Solution Swift 5.1

In App delegate implement this method

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
     if let topController = UIApplication.topViewController() {
     
        if topController.isKind(of: YourSpecificViewController.self) {
            return .all
        }
        return .portrait
     }
    return .portrait
}

Then add this extension to get the top most ViewController

extension UIApplication {
    class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
    
        if let nav = base as? UINavigationController {
            return topViewController(base: nav.visibleViewController)
            
        } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
            return topViewController(base: selected)
            
        } else if let presented = base?.presentedViewController {
            return topViewController(base: presented)
        }
        return base
    }
}
Bùi Đức Khánh
  • 3,975
  • 6
  • 27
  • 43
Rasheed
  • 131
  • 1
  • 6
0

Just wanted to share my solution as someone who has spent too much time rotating one view controller in the app:

var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { get }

overriding this UIViewController method helped me do what I need.

  1. On the view controller that you want to rotate do this for landscape left rotation:

override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return UIInterfaceOrientation.landscapeLeft }

  1. Make sure you enable rotation in the desired directions from the project settings:

enter image description here

  1. And add this to AppDelegate to disable other screens' rotation:

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

Jordan Montel
  • 8,227
  • 2
  • 35
  • 40
Murat Yasar
  • 994
  • 9
  • 24
0

Swift 5

Another answer, this one covers the isBeingDismissed case.

In AppDelegate:

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        if
            let vvc = navigationController?.visibleViewController,
            vvc is YOURViewControllerClassName &&
            !vvc.isBeingDismissed
        {
            return UIInterfaceOrientationMask.landscape
        } else {
            return UIInterfaceOrientationMask.portrait
        }
    }
Community
  • 1
  • 1
Jimmy_m
  • 1,568
  • 20
  • 24
0

None of these answers worked for me. Fundamentally, AppDelegate's method does not allow specification on which viewController. So either the topMost ViewController is rotatable, in which case the whole view controller hierarchy gets rotated, or nothing gets rotated.

However, I did find a promising answer in Child View Controller to Rotate While Parent View Controller Does Not

It references https://developer.apple.com/library/archive/qa/qa1890/_index.html

Winston Du
  • 340
  • 2
  • 9