Rotation only in one ViewController

19,177

Solution 1

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

Solution 2

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 = 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 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)
}

Solution 3

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
}

Solution 4

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)

Solution 5

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 {
}
Share:
19,177
Armin Scheithauer
Author by

Armin Scheithauer

Developing iOS Apps and Websites. Started with iOS just some months ago and website programming since 2000.

Updated on June 06, 2022

Comments

  • Armin Scheithauer
    Armin Scheithauer almost 2 years

    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

  • Lyndsey Scott
    Lyndsey Scott over 9 years
    @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
    Lyndsey Scott over 9 years
    @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
    Lyndsey Scott over 9 years
    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.
  • Himanshu Singla
    Himanshu Singla over 6 years
    Unable to do so for a viewcontroller which is inside a framework, as framework doesn't have a AppDelegate file
  • noob
    noob over 6 years
    @LyndseyScott what does that mean "disallowing transitions while that view is in landscape." Can you explain it in detail. Thanks
  • dmyma
    dmyma about 5 years
    I also added to viewWillDisappear if (self.isMovingFromParent) { UIDevice.current.setValue(Int(UIInterfaceOrientation.portrai‌​t.rawValue), forKey: "orientation") } so, the presenting vc doesn't rotate on pop.
  • Nizzam
    Nizzam almost 5 years
    I have tried your solution. Unfortunately, its not working.
  • Sara
    Sara about 4 years
    I've made all of these changes but nothing is changing for me, any other suggestions?
  • Tieda Wei
    Tieda Wei about 4 years
    May I ask where is navigationController? coming from?
  • Tieda Wei
    Tieda Wei about 4 years
    If MyRotatbleVC is presented and when it's dismissed in Landscape mode, the previous VC will in Landscape mode, instead of remaining Portrait.
  • Senocico Stelian
    Senocico Stelian about 3 years
    Great! Thank you!
  • sreejesh
    sreejesh over 2 years
    Thank you very much. Your answer worked. I have been searching for solutions since two days.