Restoring Auto-Rotate after Forcing Orientation in Swift
Asked Answered
C

1

6

In my iPhone app I have a view that I want to show only in portrait mode. When navigating to that view it should be automatically displayed in portrait view. When navigating away, the orientation should change back to what it was, or, if the device orientation has changed, adapt to that. I could find information on forcing an orientation and preventing auto-rotate. I could not find anything on how to change back to the correct orientation after navigating away from that view.

So my idea was to

  1. save the initial orientation (store in currentOrientation)
  2. subscribe to orientation change event to keep track of orientation changes while the content is locked to portrait (update currentOrientation)
  3. when leaving the view, restore the correct orientation using the currentOrientation value.

Edit (code now removed): Apart from it not working it was a dangerous way to go as it made extensive use of unsupported APIs.


Edit:

I believe this question can now be boiled down to the following:

  1. Is there a documented, supported way to force the interface orientation independent of the device orientation? setValue(UIInterfaceOrientation.Portrait.rawValue, forKey: "orientation") has been recommended many times on SO and elsewhere but it does indeed seem to be an unsupported hack.

  2. Is there a documented, supported way to update the interface orientation to the device orientation? That would be needed to "recover" from the forced interface orientation in another view without having to trigger auto rotation by turning the device back and forth.

Supported are supportedInterfaceOrientations() and shouldAutorotate(). But these will only lock the interfaceOrientation after the device has been turned to that position. They do not prevent wrong initial orientation.

There are many questions similar to this one, showing that this problem setting is not uncommon, but so far no satisfactory and complete solution using supported methods.

Culdesac answered 2/7, 2015 at 21:55 Comment(6)
What you're doing to set the orientation is undocumented and unsupported. It's probably easier to just add landscape support for this one view than to try to control the rotations.Gehring
Ideally, the system would take care of the correct orientation after leaving the orientation-locked view but it won't. Is there a simpler way to achieve this? Creating a landscape version of that view is not an option.Culdesac
You should delete the setValue(… forKey: "orientation") bit because that is undocumented and supported. You should be able to achieve what you want using supportedInterfaceOrientations(), shouldAutorotate(), and attemptRotationToDeviceOrientation().Gehring
Just curious (not being argumentative) - why isn't creating a landscape version of the view an option? I'm interested in the UX you're working on.Gehring
Thanks, Aaron, I will play around with it a bit more and try to come up with a simpler solution using supported interfaces. Yes, not going landscape is a design decision. It's a cross-platform app that started in Windows and Android where that view is a hub page or view pager respectively. It has a full-screen background image, and it is pretty much the core of the app. It would not look good in horizontal view, even if I managed to rearrange all the elements. Completely redesigning that view e.g. using tabs I feel the app would lose its (brand) id.Culdesac
One more point: the setValue() is used to force the view to portrait when it's opened while the device is in landscape. supportedInterfaceOrientations() and shouldAutoRotate() did not take care of that. I needed to turn the device to portrait first, and then the view was locked to portrait. I found several references to the setValue() method, which, of course, does not make it good practice. How else can I achieve that?Culdesac
O
1

I had a similar problem except I needed one view controller to only work in Landscape mode and another when it was in portrait. The way I achieved this was making a custom 'root' view controller. Then on the viewWillTransitionToSize method for that controller checking for orientation and non animatedly pushing the correct view controller (so it looks like a rotation to the user). And then in Interface Builder I set the view controller's orientation property explicitly instead of being inferred. You could apply this solution by having only the landscape orientation set on the restricted view controller and then on the portrait rotation doing nothing and disabling auto rotation on the restricted view controller.

Update

I haven't had the time to test any of these but these are just the ideas I used when implementing my solution for a different VC for a different orientation, some combination of the following should hopefully work I can't be a 100% certain about it cause I did this some months ago and don't exactly remember what did and didn't work.

First of all make sure that you have setup the constraints as shown in the screenshot. Mine has iPad full screen and landscape because that's what I was doing change yours to whatever you need (portrait and the size can be inferred).

View Controller Interface Builder

Now before doing anything else I would first check to see if this solved the problem. I needed the root view controller cause I needed a different VC for portrait and and a different one for landscape. You only need to restrict it so if this works than that's perfect otherwise there are a few other things you can try as mentioned below.

Once that's setup I would first go to the view controller who you want to restrict's class and prevent autorotation using:

 override func shouldAutorotate() -> Bool {
    return false
}

Now if you do that since you are restricting to portrait I'm guessing you don't really care about upside down so you don't need to do anything additional. If you do want to use the viewWillTransitionToSize method and rotate manually.

If things still don't work you can finally try the root controller way (but I would use this in the last case). Heres a sketch of it:

class VC : UIViewController { 
      override func viewDidLoad () { 
           UIDevice.currentDevice().beginGeneratingDeviceOrientationNotifications()
           NSNotificationCenter.defaultCenter().addObserver(self, selector: "orientationChanged:", name: "UIDeviceOrientationDidChangeNotification", object: nil)
         // this gives you access to notifications about rotations
      }
      func orientationChanged(sender: NSNotification)
      {
              // Here check the orientation using this:
              if UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation) { // Landscape }
              if UIInterfaceOrientationIsPortrait(UIApplication.sharedApplication().statusBarOrientation) { // Portrait }
           // Now once only allow the portrait one to go in that conditional part of the view. If you're using a navigation controller push the vc otherwise just use presentViewController:animated:
      }
}

I used the different paths for the if statements to push the one I wanted accordingly but you can do just push the portrait one manually for both and hopefully one of the ways above will help you.

Outlandish answered 3/7, 2015 at 4:34 Comment(6)
Looks promising, but I need to digest it. Do I understand correctly that your solution results in landscape-view-only (which can of course be applied the same way to portrait view)? What happens if the user navigates to that view while the device is in the non-supported orientation? Does it get stuck on the previous view until the user turns the device to the supported orientation?Culdesac
No, if the user is in landscape, since you only support portrait, when the user navigates to that controller the view will be in portrait and so the user will realize that they can't use it in landscape for that bit. Its kinda like games which only support landscape, the user realizes that they have to rotate to landscape since the view isn't rotating but in your case its only applied to a part of the application.Outlandish
Not succeeding with this but I'm not sure if I fully understood you. If time allows could you briefly sketch the root view controller code?Culdesac
Thank you for the detailed description. Unfortunately, it is not working. First of all, the actual view is only pushed when a rotation event occurs while the root view controller is loaded. After that, every orientation change triggers another instance of the main (restricted) view to be pushed on the stack until the root view controller is popped off the stack. The shouldAutoRotate() does not not seem to have any effect.Culdesac
To solve the problem of the root view controller not pushing initially just call the orientationChanged function in viewDidAppear:animated. Also, to prevent multiple pushes you need to store the current orientation and when only perform the push when the orientation changes also I forgot to mention that you have to dismiss the view controller or pop it depending on whether you're using a UINavigationController. Do this all without animation so that the user doesn't see it.Outlandish
Let us continue this discussion in chat.Culdesac

© 2022 - 2024 — McMap. All rights reserved.