I am using ARKit and I want to allow the users to use the app in both portrait and landscape mode.
I would like all UI controls to rotate on orientation change except for the ARSCNView.
I tried to transform the sceneView in the opposite direction but that didn't work.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let targetRotation = coordinator.targetTransform
let inverseRotation = targetRotation.inverted()
coordinator.animate(alongsideTransition: { (context) in
self.sceneView.transform = self.sceneView.transform.concatenating(inverseRotation)
context.viewController(forKey: UITransitionContextViewControllerKey.from)
}, completion: nil)
}
How can I prevent the scene view of the ARKit session from rotating while allowing all other UI controls to rotate on orientation change?
You cannot specify device rotation rules on view basis. It has to be set on view controller basis. This is how iOS works. Thus, to achieve what you need you have to handle this by yourself. For example, if you're showing your ARSCNView as a full screen view, then you can present it inside a custom UIViewController sub-class, and set the rotation configuration for that controller.
Setting the supported view rotations for a specific view controller can be implemented in many ways, below are some of them.
Approach #1:
You can set the supported view orientations for any UIViewController by overriding your app delegate's method application:supportedInterfaceOrientationsForWindow:.
Sample code:
// this goes into your AppDelegate...
// Basically, this checks the current visible view controller type and decide
// what orientations your app supports based on that view controller type (class)
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
let visibleViewController = self.topViewController(withRootViewController: window?.rootViewController)
// Support only portrait orientation for a specific view controller
if visibleViewController is SomeViewController {
return .portrait
}
// Otherwise, support all orientations (standard behaviour)
return .allButUpsideDown
}
// This simple helper method is to extract the exact visible view controller at
// the moment, as `window.rootViewController` could have some container controller
// like `UINavigationController` or so that holds more controllers into it
private func topViewController(withRootViewController rootViewController: UIViewController?) -> UIViewController? {
if let rootViewController = rootViewController as? UITabBarController {
return rootViewController.selectedViewController
} else if let rootViewController = rootViewController as? UINavigationController {
return rootViewController.visibleViewController
} else if let presentedViewController = rootViewController?.presentedViewController {
return topViewController(withRootViewController: presentedViewController)
}
return rootViewController
}
Reference:
application:supportedInterfaceOrientationsForWindow: method documentation
Approach #2:
Sub-class your UINavigationController and override the following property:
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return self.topViewController?.supportedInterfaceOrientations ?? .all
}
Now this always looks into your view controller's supportedInterfaceOrientations and set the supported orientations based on the return value. Doing this enables you to simply override this in any view controller you want, setting some custom value.
For example, in SomeViewController you could simply add:
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
Reference:
supportedInterfaceOrientations property documentation
Approach #3:
If you don't want to sub-class your UINavigationController like in approach #2 above, you can set your SomeViewController as the navigation controller delegate implementing navigationControllerSupportedInterfaceOrientations(:)
Sample code:
override func viewDidLoad() {
super.viewDidLoad()
// ...
self.navigationController?.delegate = self
}
// MARK: - UINavigationControllerDelegate
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return navigationController.topViewController?.supportedInterfaceOrientations ?? .all
}
Reference:
Matt's answer here
Related
I am trying to prevent rotation (lock it to say, portrait) in a specific VC that is
embedded in a navigation controller.
I am currently doing this:
To UINavigationController
extension UINavigationController {
public override func supportedInterfaceOrientations() -> Int {
return visibleViewController.supportedInterfaceOrientations()
}
}
In my VC:
class ViewController: UIViewController {
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
}
However, I have issues when we go to another VC (embedded in another nav controller) is presented which supports both landscape and portrait. Suppose, the user rotates in the new screen to landscape. And clicks back to go to original screen. The app is now presented in landscape as opposed to portrait defined in its supportedInterfaceOrientations override. How do I prevent this erroneous behaviour?
I read in iOS 11, we should use viewWillTransition(to:with:) to handle rotation (and locking as well). In UIViewController documentation
“As of iOS 8, all rotation-related methods are deprecated. Instead,
rotations are treated as a change in the size of the view controller’s
view and are therefore reported using the viewWillTransition(to:with:)
method. When the interface orientation changes, UIKit calls this
method on the window’s root view controller. That view controller then
notifies its child view controllers, propagating the message
throughout the view controller hierarchy.”
Can you give directions on how to achieve it?
You can use this cool utility that I've been using.
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")
}
}
You can also rotate your screen and at the same time, lock it to that orientation. I hope this helps!
You can use supportedInterfaceOrientationsFor
Define this in your AppDelegate
var restrictRotation = Bool()
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if restrictRotation {
return .portrait
}
else {
return .all
}
}
Put below code in your ViewController
func restrictRotation(_ restriction: Bool) {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.restrictRotation = restriction
}
call above function in your ViewController ViewwillAppear like this.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.restrictRotation(true) // TRUE MEANS ONLY PORTRAIT MODE
//OR
self.restrictRotation(false) // FALSE MEANS ROTATE IN ALL DIRECTIONS
}
My App is basically a portrait only app. So nothing is rotating. But not there is ONE exception. The user can add photos and when viewing those photos full-screen, this ViewController SHOULD be allowed to rotate.
So I thought that if my ViewController that is presenting has supportedInterfaceOrientations return .portrait and also shouldAutorotate return false, that this should be enough, to prevent that one from rotation?!?
Turns out, that when I rotate while having the full-screen image presented, the one underneath is rotated as well.
To summarize:
RootViewController should NEVER rotate
PresentedViewController can rotate, but his rotation should no rotate the RootViewController
Is there a way to achieve that?
Probably too late, but in case if somebody will be faced with the same issue, i would provide my solution.
Actually suppress rotation of underlying window is possible if set modalPresentationStyle = .fullScreen for presented controller, then if you take a look of "View UI Hierarchy" then you can notice that controller which represents controller in fullscreen will be removed from controllers hierarchy. But at the same time modalPresentationStyle = .overFullScreen keep everything as is, what causing rotating underlying controller even it set by default supported orientation to portrait, i.e. UIWindow who manages and routes system events over hierarchy respect settings of the toppest controller in case of modalPresentationStyle = .overFullScreen. So according to the facts, and if it is necessary to have e.g. custom presentation, i would suggest to use additional UIWindow which will be responsible for the presenting controller in fullscreen.
i have implemented test project for the solution: here
You can give an exception like in AppDelegate:
//auto rotate
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
//landscape for perticular view controller
let navigationController = window?.rootViewController as? UINavigationController
if let activeController = navigationController?.visibleViewController {
if activeController.isKind(of: VGVideoVC.self) {//Pass your VC here
// print("I have found my controller!")
return UIInterfaceOrientationMask.all;
}else{
return UIInterfaceOrientationMask.portrait;
}
}else{
return UIInterfaceOrientationMask.portrait;
}
}
And in the Rest of the VC where you want it to be forcefully portrait, you can use like this:
//MARK:- Screen Orientation
override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
return .portrait
}
override var shouldAutorotate: Bool{
return true
}
Hope this helps.
I would say disable the orientation change for the complete app and listen to device orientation change in Photos view controller and update the UI of photosVC on device orientation change.
Something like this:
NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: Notification.Name("UIDeviceOrientationDidChangeNotification"), object: nil)
#objc func orientationChanged() {
if(UIDeviceOrientationIsLandscape(UIDevice.current.orientation)){
print("landscape")
}
if(UIDeviceOrientationIsPortrait(UIDevice.current.orientation)){
print("Portrait")
}
}
Be careful with the upside down and other orientations which you don't need.
Try this code below. I followed This tutorial and it works for me. What's going on is:
Step 1. Assuming inside General your Device Orientation is set to Portrait only:
Step 2. The code below that you add inside AppDelegate loops through the navigation controllers and then looks inside their top view controllers. If any of those vcs have a function with the name canRotate then that specific vc will change the device orientation from Step 1. by returning: return .allButUpsideDown
Add these 2 functions to the bottom of your AppDelegate:
// add this first function
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
// if the navigationController's root vc has a function inside of it named canRotate
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). vcs that don't contain a function with the name "canRotate" can't rotate and stay in portrait only
return .portrait;
}
// add this second function
// loop through tabBarController or any navigationControllers
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
}
Step 3. Inside the modal vc you should add a function named: #objc func canRotate(){}. You don't have to call it anywhere or add anything inside it's curly braces. The code from Step 2 is looking for this function with the name canRotate. If the other vcs don't contain a function with that name then they can't rotate.
Inside the modal viewController that you want to rotate add the canRotate() function anywhere outside of viewDidLoad and inside viewWillDisappear add the code to set everything back to your regular Portrait only :
override func viewDidLoad() {
super.viewDidLoad()
}
#objc func canRotate(){}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// add this so once leaving this vc everything will go back to Portrait only
if (self.isMovingFromParentViewController) {
UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
}
}
my app is portrait what i wanna do is allow 1 view controller to be portrait or landscape i tried using this code in AppDelegate but i am getting an error
func application(_ application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow) -> Int {
let currentViewController: UIViewController? = self.topView
// Get topmost/visible view controller
var currentViewController: UIViewController? = self.topViewController
// Check whether it implements a dummy methods called canRotate
if currentViewController?.responds(to: #selector(self.canRotate)) {
// Unlock landscape view orientations for this view controller
return .allButUpsideDown
}
// Only allow portrait (standard behaviour)
return .portrait
}
}
Does anyone know how to do that in swift?
I took the following approach in my app.
Create a custom UINavigationController with the following code & make sure that your root navigation controller is updated to use this as its class.
import UIKit
class NavigationController: UINavigationController {
override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
if let _ = presentedViewController as? MyPortraitViewController {
return .portrait
}
return .allButUpsideDown
}
}
You can replace MyPortraitViewController with the class name of the view controller you wish to deviate from the global setting.
While I use .portrait in this example, you are free to use other options.
One implementation example would be to replace the class of your base navigation controller in your storyboard which is shown below.
I have an app where I set the orientation to be Portrait only in it's target settings:
In one particular view controller I'd like to override this setting so auto layout will update views when a device is rotated. I've tried these methods with no success:
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.AllButUpsideDown
}
If I have read the accepted answer, I wouldn't have to write my own. My version is longer but only require app delegate's application(_:supportedInterfaceOrientationsFor:) method. It maybe suitable in situation where you can't change or prefer not to change the target view controllers. For example, a third party view controller.
I was inspired from Apple's official doc: supportedInterfaceOrientations
My app runs as Portrait on iPhone and all orientations on iPad. I only want one view controller (a JTSImageViewController presents an image for larger view) to be able to rotate.
Info.plist
Supported interface orientations = Portrait
Supported interface orientations (iPad) = Portrait, PortraitUpsideDown, LandscapeLeft, LandscapeRight
If app delegate implemented application(_:supportedInterfaceOrientationsFor:), Info.plist maybe ignored. However, I didn't verify that.
Swift 4
func application(_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
// Early return for iPad
if UIDevice.current.userInterfaceIdiom == .pad {
return [.all]
}
// 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 JTSImageViewController) && !(vc!.isBeingDismissed) {
return [.allButUpsideDown]
}
return [.portrait]
}
Your code in the desired VC will not work.
I've managed this by adding the following code in the AppDelegate
var autoRotation: Bool = false
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
return autoRotation ? .AllButUpsideDown : .Portrait
}
And then you can make an helper class and add this method:
class func setAutoRotation(value: Bool) {
if let appDelegate = UIApplication.sharedApplication().delegate as? AppDelegate {
appDelegate.autoRotation = value
}
}
Finally in your desired VC, you can call setAutoRotation(true) on didLoad and
setAutoRotation(false) on willDissapear.
This can also achieved by subclassing the UINavigationController. You can find the answer here.
Hope it helps
First of all, this isn't a duplicate. I've looked at all of the questions related to this on SO and none of them work for me. Hopefully it's just because I'm new to iOS development but I suspect this isn't possible in my case. I've attached a picture with the view controller circled that I want to disable rotation for.
I've already tried: Subclassing the view controller that I want to disable rotation for and using the shouldAutoRotate method by setting it to NO. Apparently this doesn't work because it's the navigation controller that dictates whether its view controllers can rotate. So, I subclassed UINavigationController and used this class in storyboard instead of the default navigation controller. In this class I've set shouldAutoRotate to NO. It still doesn't work. Don't really know what I'm doing wrong.
When I extend the root view controller with my view controller class with shouldAutoRotate set to NO, it disables rotation...for the whole app. This is not what I want. I only want the rotation to be disabled for the view controller circled in the picture.
Thanks in advance!
Add your AppDelegate.h
#property (nonatomic , assign) bool blockRotation;
AppDelegate.m
-(NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if (self.blockRotation) {
return UIInterfaceOrientationMaskPortrait;
}
return UIInterfaceOrientationMaskAll;
}
In view Controller that you want to disable rotation
- (void)viewDidLoad
{
[super viewDidLoad];
AppDelegate* shared=[UIApplication sharedApplication].delegate;
shared.blockRotation=YES;
}
-(void)viewWillDisappear:(BOOL)animated{
AppDelegate* shared=[UIApplication sharedApplication].delegate;
shared.blockRotation=NO;
}
Swift 4.2 and later:
In your AppDelegate.swift:
var blockRotation = false
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if blockRotation {
return .portrait
}
return .all
}
In your viewController:
override func viewDidLoad() {
super.viewDidLoad()
AppDelegate.shared.blockRotation = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
AppDelegate.shared.blockRotation = false
}
Thanks to #ozgur for the fix which worked for me. I would 'vote up', but apparently I'm not good enough (whatever!) to vote whether a fix works or not. Anyway, here it is in Swift:
In AppDelegate.swift:
class AppDelegate: UIResponder, UIApplicationDelegate {
var blockRotation: Bool = false
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> Int {
if (self.blockRotation) {
println("supportedInterfaceOrientations - PORTRAIT")
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
} else {
println("supportedInterfaceOrientations - ALL")
return Int(UIInterfaceOrientationMask.All.rawValue)
}
}
In the ViewController that you want to block rotation, add UIApplicationDelegate to your class...
class LoginViewController: UIViewController, UITextFieldDelegate, UIApplicationDelegate {
and then create a reference to the AppDelegate...
var appDelegate = UIApplication.sharedApplication().delegate as AppDelegate
In viewDidLoad, set appDelegate.blockRotation = true:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
appDelegate.blockRotation = true
}
In viewWillAppear, set the orientation to force the device to the chosen orientation (Portrait in this example):
override func viewWillAppear(animated: Bool) {
let value = UIInterfaceOrientation.Portrait.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
}
Then in viewWillDisappear, or in prepareForSegue, set appDelegate.blockRotation = false:
override func viewWillDisappear(animated: Bool) {
appDelegate.blockRotation = false
}
This worked for me in this exact (multiple view controllers in a Navigation Controller) scenario, after many hours of reading other solutions on this site. Thanks again to #ozgur - I'd up-vote your answer if I could!
Happy trails!
You have to check in your root view controller is rotation allowed in current top controller and return YES or NO in supportedInterfaceOrientations method. So it should be like the following code in your root controller (adapt the code to your case):
- (NSUInteger)supportedInterfaceOrientations
{
return [self.navigationController.topViewController supportedInterfaceOrientations];
}
Then in each view controller add supported interface orientations, for example:
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
For those of you using Swift 2, you can follow Andre's answer, but in step one, use the code below in your AppDelegate.swift:
class AppDelegate: UIResponder, UIApplicationDelegate {
var blockRotation: Bool = false
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if (self.blockRotation) {
print("supportedInterfaceOrientations - PORTRAIT")
return UIInterfaceOrientationMask.Portrait
} else {
print("supportedInterfaceOrientations - ALL")
return UIInterfaceOrientationMask.All
}
}
supportedInterfaceOrientationsForWindow: now returns a UIInterfaceOrientationMask instead of an Int.
My case has 3 view controller:
- first view controller: portrait
- second view controller: landscape right (has navigation controller and was presented by first view controller)
- third view controller: portrait (has navigation controller and was pushed by second view controller )
And this is my solution in swift 3:
------------------------------------
At AppDelegate:
- Add this property to save your setting
private var orientation: UIInterfaceOrientationMask = .portrait
-Then create a function to set rotate for your device:
func rotateScreen(orientation: UIInterfaceOrientationMask) {
self.orientation = orientation
var value = 0;
if orientation == .landscapeRight {
value = UIInterfaceOrientation.landscapeRight.rawValue
}else {
value = UIInterfaceOrientation.portrait.rawValue
}
UIDevice.current.setValue(value, forKey: "orientation")
}
- Finally, implement support orientation method:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return self.orientation
}
--------------------------------
Then you can call like this before display destination view controller
(UIApplication.shared.delegate as! AppDelegate).rotateScreen(orientation: .landscapeRight)
For example, in my case: When user tap on button at the First to present the Second, I'll do like this
(UIApplication.shared.delegate as! AppDelegate).rotateScreen(orientation: .landscapeRight)
self.present(navigation, animated: true, completion: nil)
And the close button at the Second I'll do like this
(UIApplication.shared.delegate as! AppDelegate).rotateScreen(orientation: .portrait)
self.dismiss(animated: true, completion: nil)
That's all! You could never mind using navigation controller or not.
Hope this helps you guys ^__^!
UINavigationController+Rotation Category
When u usign navigation controller SupportedInterfaceOrientaions of ViewController will not work adding this category to ur Project will get the response from your viewController also instead of NavigationController alone.
In my case I have the view controller embedded in a navigation controller, and most of the solutions don't work because the viewcontroller depends on the navigation controller orientation. For this when you create the instance of the view controller you have to cast it to UINavigationController :
let theViewController = UIViewController()
if let navController = theViewController as? UINavigationController {
navController.delegate = self
}
And then add this extension:
extension PlaySplashViewController: UINavigationControllerDelegate {
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.portrait
}
}
In this case this going to set portrait orientation for the navigation controller so the view controller going to use this orientation too.