presentedViewController changes orientation of presenting view controller, breaks layout - ios

I have a ViewController A that presents an MPMoviePlayerViewController. I want to force the video player to landscape, so I use this:
In the AppDelegate, supportedInterfaceOrientationsForWindow
if let viewController = self.window!.rootViewController?.presentedViewController {
if let playbackController = viewController as? MPMoviePlayerViewController {
return Int(UIInterfaceOrientationMask.LandscapeLeft.rawValue) | Int(UIInterfaceOrientationMask.LandscapeRight.rawValue)
}
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
In my video player subclass, I set the supported orientation:
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.LandscapeLeft.rawValue) | Int(UIInterfaceOrientationMask.LandscapeRight.rawValue)
}
When the video player is closed in landscape, however, View Controller A is stuck in landscape. How do I prevent View Controller A from switching to landscape, and keep it in portrait?

Just add
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Portrait.rawValue)
}
to every view controller that needs it and avoid AppDelegate based orientation support.

Related

Preventing rotation in a specific view controller in iOS 11

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
}

Prevent UIViewController from rotating when modal is rotating

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

Not able to lock orientation for 1 VC

I am trying to get device rotation right.
I am testing on iPad 8.x/9.x simulator
I have 4 VCs
VC1 - Both Portrait and Landscape
VC2 - Both Portrait and Landscape
VC3 - Only Portrait
VC4 - Both Portrait and Landscape
Goal: to have VC3 display PortraitView at all times (same as if app orientation was fixed to portrait).
I tried
#implementation RotationAwareNavigationController
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIViewController *top = self.topViewController;
return top.supportedInterfaceOrientations;
}
-(BOOL)shouldAutorotate {
UIViewController *top = self.topViewController;
return [top shouldAutorotate];
}
#end
In VC which is portrait
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskPortrait;
}
But it does not work meaning view not displayed in Portrait dimensions Am I missing something?
I am sure it can be done as when I use ImagePickerController provided my iOS, it is fixed to Portrait. I just dont know how to do it.
To support differing orientations in different view controllers, you will need to do a few things. First, you need to check all the checkboxes for orientations you want to support in your target's General settings tab.
Second, anytime you call presentViewController or dismissViewController in your app, the UIApplicationDelegate method application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask will be called. You can use this method to restrict (or allow) specific orientations each time a new view controller is presented or dismissed. Unfortunately, it isn't as simple as just returning a UIInterfaceOrientationMask here. You will need to find the view controller that is going to be showing on screen and return the orientations that it supports. Here is an example of this:
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
guard let window = window, let rootViewController = window.rootViewController else {
return UIDevice.currentDevice().userInterfaceIdiom == .Pad ? .All : .AllButUpsideDown // iOS defaults
}
// Let the view controller being shown decide what orientation it wants to support. This method will be called anytime a new view controller is presented on screen.
return findVisibleViewController(rootViewController: rootViewController).supportedInterfaceOrientations()
}
/// Searches the view hierarchy recursively and finds the view controller that is currently showing.
private func findVisibleViewController(rootViewController rootViewController: UIViewController) -> UIViewController {
if let presentedViewController = rootViewController.presentedViewController {
// Search for a modal view first.
return self.findVisibleViewController(rootViewController: presentedViewController)
} else if
let navigationController = rootViewController as? UINavigationController,
let visibleViewController = navigationController.visibleViewController {
// Then search navigation controller's views to find its visible controller.
return self.findVisibleViewController(rootViewController: visibleViewController)
} else if let splitViewController = rootViewController as? UISplitViewController {
// Then try the split view controller. This will be the true root view controller. Use the master here since the detail just shows web views.
return self.findVisibleViewController(rootViewController: splitViewController.viewControllers[0])
} else {
// Guess we found the visible view controller, because none of the other conditions were met.
return rootViewController
}
}
findVisibleViewController(_:) is an example from one of my projects and is tailored to the exact view controller hierarchy in my app. You will need to edit this for your own app in a way that makes sense for your hierarchy.
Third, you will need to implement supportedInterfaceOrientations() for most, if not all, of your view controllers.
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return // portrait, landscape, or combinations of those.
}
Finally, this only handles situations where you presented or dismissed something modally. For show (push) segues, the orientation of the navigation controller will be used for every new view controller pushed onto the stack. If you need more fine grained control here, you will need to force the orientation to happen. Here is an example of that:
// Some other view had the screen in landscape, so force the view to return to portrait
UIDevice.currentDevice().setValue(UIInterfaceOrientation.Portrait.rawValue, forKey: "orientation")

Lock one view orientation in iOS

I'm trying to lock one view of my app in portrait mode. I have a top navigation controller view and multiple nested scenes.
I found many answers to lock orientation question and tried the code below, but it works only partially. Let the scenes A and B, where A has a segue to B, and B is locked in portrait orientation. If I go to the B scene, turn the device to the landscape position and back to A, the orientation is inherited from previous B (portrait).
How can I make the A scene recognize the actual device orientation?
Navigation controller
internal override func shouldAutorotate() -> Bool {
return visibleViewController!.shouldAutorotate()
}
"B" View controller
override func shouldAutorotate() -> Bool {
if UIDevice.currentDevice().orientation == .Portrait {
return true
}
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
override func viewDidLoad() {
super.viewDidLoad()
let orientation = UIInterfaceOrientation.Portrait.rawValue
UIDevice.currentDevice().setValue(orientation, forKey: "orientation")
}
As I understand your question you want to know how to get the current device orientation.
Try this...
var orientation = UIDevice.currentDevice().orientation

iOS How to replicate Youtube App player rotation?

Any idea about how the feature I show in this video is done?
It looks like the touch on the "expand" button force an orientation change for the device (infact when I swipe I'm pulling the control centre out).
Anyway only the video player seems to rotate. the scrollview underneath keeps its portrait orientation.
Looks like they're presenting the player controller with a custom segue animation, while having the player controller supporting only landscape.
Try presenting a view controller that has these implemented, like so:
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
override func preferredInterfaceOrientationForPresentation() -> UIInterfaceOrientation {
return UIInterfaceOrientation.LandscapeLeft
}
override func shouldAutorotate() -> Bool {
return false
}
Then the device should be in landscape. All you have left is to do the custom transition animation between the two and you're done.
From the docs:
preferredInterfaceOrientationForPresentation
If your view controller implements this method, your view controller’s
view is shown in the preferred orientation

Resources