Skip to content

Instantly share code, notes, and snippets.

@ajself
Last active May 27, 2024 12:11
Show Gist options
  • Save ajself/173778ad6104870b541c7549805801e0 to your computer and use it in GitHub Desktop.
Save ajself/173778ad6104870b541c7549805801e0 to your computer and use it in GitHub Desktop.
Rotate UIViewControllers that conform to a protocol
/*
This is an update to an example found on http://www.jairobjunior.com/blog/2016/03/05/how-to-rotate-only-one-view-controller-to-landscape-in-ios-slash-swift/
The code there works, with some updating to the latest Swift, but the pattern isn't very Swifty. The following is what I found to be more helpful.
*/
/*
First, create a protocol that UIViewController's can conform to.
This is in opposition to using `Selector()` and checking for the presence of an empty function.
*/
/// UIViewControllers adopting this protocol will automatically be opted into rotating to all but bottom rotation.
///
/// - Important:
/// You must call resetToPortrait as the view controller is removed from view. Example:
///
/// ```
/// override func viewWillDisappear(_ animated: Bool) {
/// super.viewWillDisappear(animated)
///
/// if isMovingFromParentViewController {
/// resetToPortrait()
/// }
/// }
/// ```
protocol Rotatable: AnyObject {
func resetToPortrait()
}
extension Rotatable where Self: UIViewController {
func resetToPortrait() {
UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
}
}
/*
Next, extend AppDelegate to check for VCs that conform to Rotatable. If they do allow device rotation.
Remember, it's up to the conforming VC to reset the device rotation back to portrait.
*/
// MARK: - Device rotation support
extension AppDelegate {
// The app disables rotation for all view controllers except for a few that opt-in by conforming to the Rotatable protocol
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
guard
let _ = topViewController(for: window?.rootViewController) as? Rotatable
else { return .portrait }
return .allButUpsideDown
}
private func topViewController(for rootViewController: UIViewController!) -> UIViewController? {
guard let rootVC = rootViewController else { return nil }
if rootVC is UITabBarController {
let rootTabBarVC = rootVC as! UITabBarController
return topViewController(for: rootTabBarVC.selectedViewController)
} else if rootVC is UINavigationController {
let rootNavVC = rootVC as! UINavigationController
return topViewController(for: rootNavVC.visibleViewController)
} else if let rootPresentedVC = rootVC.presentedViewController {
return topViewController(for: rootPresentedVC)
}
return rootViewController
}
}
@TomQDRS
Copy link

TomQDRS commented Nov 21, 2018

Thank you so much for this gist, it's been really useful. I've been running into a small problem though.

I need a view in my hierarchy to be rotatable to present a video. However, the view before, which it will return to, can absolutely not be rotated (it's a complicated view that breaks if it is rotated). Since I'm using AVPlayerViewController, I can't actually catch the "done" button action from the user to reset the rotation before asking to dismiss the controller.

Calling resetToPortrait in viewWillDisappear of the AVPlayerViewController sadly doesn't help, because the rotation will somehow only follow after the view has already disappeared. Even when presenting the AVPlayerViewController from another ViewController that is dismissed when AVPlayerViewController is dismissed, the rotation occurs too late.

Do you have a suggestion for making a controller wait for the rotation to actually occur before it dismisses itself?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment