Lock Screen Rotation in iOS 8



Locking rotation for the entire app

There is a lot to locking screen rotation. Read this in its entirety if you want to better understand screen rotation otherwise jump to the Best Implementation. Usually you'll want rotation to work the same throughout an entire app. To do this you simply click on the project in the Project Navigator, click on your target in the Target list, click on the General tab and check the device orientations you want under Deployment Info -> Device Orientation.

When to lock rotation for a specific view controller

Locking rotation for a single view controller is no where as easy as locking it for the entire app. We are obviously in edge case territory here. There are good cases for having rotation work differently in a specific view of the app. I found this to be the case with an app I'm working on currently where there is an Augmented Reality view that helps the user locate an underground object with that object overlaying a camera view. Because the user is moving the iPad around trying to locate that object it is annoying to have it rotate on them. Similarly if you wanted a camera view that only took landscape photos that would be another good reason to lock just 1 view controller. Luckily in my case I just keep them in whatever rotation they start in and just don't let them rotate. If it is inside a Navigation or Tab Bar Controller then it is going to require more work. Keep this in mind and weigh the pros and cons of what you are trying to do. Maybe you should consider locking rotation to that rotation for the entire app. This is especially common with game apps and it might not be worth the investment to get everything to play nicely together.

Root or Presented View Controller

If you are not embedded inside another controller then these instructions will work for you.

Lock to the rotation they start in

This way is easiest. Just return false to the shouldAutorotate method.

class ViewController: UIViewController {
    override func shouldAutorotate() -> Bool {
	return false
    }
}

Lock to a specific orientation

Locking to a specific orientation is trivial as well. Just make sure you don't override the shouldAutorotate method because it does need to rotate to get into the correct position.

class ViewController: UIViewController {
    override func supportedInterfaceOrientations() -> Int {
	return Int(UIInterfaceOrientationMask.Landscape.rawValue)
    }
}

Navigation Controller

If your view controller belongs to a navigation controller it has to obey the rules of the navigation controller even if it has different orientation rules itself.

Locked to current orientation

I found the easiest way to to do this is by writing a class extension on UINavigationController. Notice that this isn't the ideal way since UINavigationController now knows a name of the class ViewController and it is deciding what behavior to do with that class. While this does work it is a naive design, for a better approach Follow rules of encapsulation.

extension UINavigationController {
    public override func shouldAutorotate() -> Bool {
	if visibleViewController is ViewController {
	    return false
	}
	return true
    }
}

Autorotated to specific orientation

This is also technically wrong in the world of OO and you should Follow rules of encapsulation instead.

extension UINavigationController {
    public override func supportedInterfaceOrientations() -> Int {
	if visibleViewController is ViewController {
	    return Int(UIInterfaceOrientationMask.Landscape.rawValue)
	}
	return Int(UIInterfaceOrientationMask.All.rawValue)
    }
}

Follow rules of encapsulation

A better approach would be to let the view controller decide for itself and the navigation controller will use the decision of the top most view controller. This is much better than the Navigation Controller knowing the name of a particular view controller and also better than choosing the behavior for that view controller itself.

Locked to current orientation

extension UINavigationController {
    public override func shouldAutorotate() -> Bool {
	return visibleViewController.shouldAutorotate()
    }
}

class ViewController: UIViewController {
    override func shouldAutorotate() -> Bool {
	return false
    }
}

Autorotated to specific orientation

extension UINavigationController {
    public override func supportedInterfaceOrientations() -> Int {
	return visibleViewController.supportedInterfaceOrientations()
    }
}

class ViewController: UIViewController {
    override func supportedInterfaceOrientations() -> Int {
	return Int(UIInterfaceOrientationMask.Landscape.rawValue)
    }
}

Another reason this design is a good one is that it is generic to any view controller and is harmless because UINavigationController inherits from UIViewController. This means that the default implementations of a view controller without overriding will be the same exact default implementation of UINavigationController. We can now support both locking to current orientation and autorotating to lock on a specific orientation with this generic extension on UINavigationController:

extension UINavigationController {
    public override func supportedInterfaceOrientations() -> Int {
	return visibleViewController.supportedInterfaceOrientations()
    }
    public override func shouldAutorotate() -> Bool {
	return visibleViewController.shouldAutorotate()
    }
}

Ok so things are looking up. But stop and think for a second. What about more complex view hierarchies. For instance a tab bar controller inside a navigation controller. Will this still work. Yes! UITabBarController also inherits from UIViewController and it can also be the navigation controller's visibleViewController. Read on and see just how extendible this can be.

UITabBarController

So how can we do this for UITabBarController? The same way! Now you might want to make this generic to UIViewController. The problem is that these methods come from a built-in extension already on UIViewController and if you write to an extension to replace the existing ones then you'll have to put in default values and the ones in the project plist will be ignored. This is not ideal since we are ignoring those values and pulling the values from the plist would be a pain as well and additionally there is already an extension with these methods and it isn't clear that we are replacing them since they can't be overriden from an extension, but here is the implementation if you wanted to that (but seriously don't do this):

extension UIViewController {
    func shouldAutorotate() -> Bool {
	if let navigation = self as? UINavigationController {
	    return navigation.visibleViewController.shouldAutorotate()
	} else if let tabController = self as? UITabBarController {
	    if let selected = tabController.selectedViewController {
		return selected.shouldAutorotate()
	    }
	}
	return true
    }

    func supportedInterfaceOrientations() -> Int {
	if let navigation = self as? UINavigationController {
	    return navigation.visibleViewController.supportedInterfaceOrientations()
	} else if let tabController = self as? UITabBarController {
	    if let selected = tabController.selectedViewController {
		return selected.supportedInterfaceOrientations()
	    }
	}
	return Int(UIInterfaceOrientationMask.All.rawValue)
    }
}

Ok so let's do this in a more readable way by letting the default values get set as expected.

extension UINavigationController {
    public override func supportedInterfaceOrientations() -> Int {
	return visibleViewController.supportedInterfaceOrientations()
    }
    public override func shouldAutorotate() -> Bool {
	return visibleViewController.shouldAutorotate()
    }
}

extension UITabBarController {
    public override func supportedInterfaceOrientations() -> Int {
	if let selected = selectedViewController {
	    return selected.supportedInterfaceOrientations()
	}
	return super.supportedInterfaceOrientations()
    }
    public override func shouldAutorotate() -> Bool {
	if let selected = selectedViewController {
	    return selected.shouldAutorotate()
	}
	return super.shouldAutorotate()
    }
}

Now it will respect whatever we decide to do in the view controller:

class ViewController: UIViewController {
    override func supportedInterfaceOrientations() -> Int {
	return Int(UIInterfaceOrientationMask.Portrait.rawValue)
    }
}

There is an issue with tab bar controllers on the initial screen launch depending on the configuration it seems to want to use a default configuration. Adding this key-value pair to the Info.plist file seemed to fix it for me.

<key>UIInterfaceOrientation</key>
<string>UIInterfaceOrientationPortrait</string>

This issue that seems to be a bug is part of the reason I like to stay away from edge cases like this where we have a view controller that rotates differently than the rest. If modifying the plist doesn't work here is a hack I try to never do but it will force the correct orientation.

class ViewController: UIViewController {
    override func supportedInterfaceOrientations() -> Int {
	return Int(UIInterfaceOrientationMask.Landscape.rawValue)
    }

    var hadFirstAppearance = false
    override func viewDidAppear(animated: Bool) {
	super.viewDidAppear(animated)

	if !hadFirstAppearance {
	    if Int(interfaceOrientation.rawValue) != Int(UIInterfaceOrientationMask.Landscape.rawValue) {
		tabBarController!.presentViewController(UIViewController(), animated: false, completion: { () -> Void in
		    dispatch_after(0, dispatch_get_main_queue()) {
			self.tabBarController!.dismissViewControllerAnimated(false, completion: nil)
		    }
		})
	    }
	}

	hadFirstAppearance = true
    }
}

Complex Hierarchies

Assuming you don't have to workaround an issue like the one described in the UITabBarController section above then you can have complex hierarchies of navigation inside tab bar or tab bar inside navigation without doing anything different. Just use these extensions and override these functions in the specific view controllers and if absolutely necessary force the orientation by presenting and dismissing a view controller.

Best Implementation

Once again, here is the best implementation I could come up with. Each view controller that needs to be special-cased will override the supportedInterfaceOrientations and shouldAutorotate methods. If issues arise see the UITabBarController section above.

extension UINavigationController {
    public override func supportedInterfaceOrientations() -> Int {
	return visibleViewController.supportedInterfaceOrientations()
    }
    public override func shouldAutorotate() -> Bool {
	return visibleViewController.shouldAutorotate()
    }
}

extension UITabBarController {
    public override func supportedInterfaceOrientations() -> Int {
	if let selected = selectedViewController {
	    return selected.supportedInterfaceOrientations()
	}
	return super.supportedInterfaceOrientations()
    }
    public override func shouldAutorotate() -> Bool {
	if let selected = selectedViewController {
	    return selected.shouldAutorotate()
	}
	return super.shouldAutorotate()
    }
}

To lock rotation in code or to not

My personal preference is to checkbox the orientations I want to lock to in the project file's Deployment info and then not put in any code anywhere. My second favorite way would be to special-case a view controller to keep whatever rotation it comes in. That is significantly less complex and only requires overriding the shouldAutorotate method in the view controller and the controllers it is embedded in (using the extensions). My last favorite option would be to special-case a particular orientation, like Landscape, and be configured such that a forced refresh has to occur. Actually worse than that would be an implementation that required code in every single view controller or for every view controller to inherit from a UIViewController subclass. Just keep it as easy as possible and hope it still works with the next iOS version. If you can find a way to remove special-case rotation requirements that would be best.

Author: Korey Hinton

Created: 2015-02-03 Tue 13:31

Emacs 24.4.1 (Org mode 8.2.10)

Validate