Clarifications on managing orientations in UIViewController - ios

I have a configuration like the following:
UINavigationController
-> ViewController
where ViewController is the root view controller for the navigation controller.
ViewController can present modally another view controller, ModalViewController, like the following.
self.presentViewController(modalViewController, animated: true, completion: nil)
Within ModalViewController I override the following method. The modal view controller in fact can be presented only in Landscape mode.
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return .Landscape
}
I then override ViewController's method for responding to orientation changes.
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
print("Size is \(size)")
}
What I noticed is that if the modal view controller is presented, the print in ViewController is logged only if ModalViewController is in Landscape mode, while it is no logged in Portrait. In other words, while I rotate the device, ModalViewController should be able to display Landscape orientations only. Under the hood, even if ViewController is not visible (and the device is in Portrait), this controller should be able to respond to size changes. This is not the case since I cannot see the print log.
Use case:
if ModalViewController is not visible this is the print log
Size is (1024.0, 768.0)
Size is (768.0, 1024.0)
Size is (1024.0, 768.0)
Size is (768.0, 1024.0)
when ModalViewController is presented modally
Size is (1024.0, 768.0)
Size is (1024.0, 768.0)
Is this one the correct behaviour? My goal is to respond to orientation changes for the ViewController even if (when ModalViewController is opened) the device is in Portrait mode. Any clue?
Edit
Based on matt comment.
If ViewController is not the frontmost view controller it has no business "responding to orientation changes". You've merely stumbled across an implementation detail or edge case in which you should have no interest.
ViewController is a complex controller that acts as a parent view controller. It has two children: PortraitViewController and LandscapeViewController. These controllers are swapped in viewWillTransitionToSize method. Whenever ModalViewController is not visible (not presented) the swapping works in the correct manner. On the contrary, when ModalViewController is presented, the swapping runs just for Landscape mode (see logs above).

It sounds to me as if you're just trying to do something at the wrong time. When the modal view controller is dismissed and your view controller's view is about to reappear, you'll get plenty of warning (including appear and layout events) and you can do whatever needs doing, based on the current orientation, before your view becomes visible to the user.

Related

Custom camera view overlay size problems when app rotates

I am using a custom camera view overlay in Swift 3. When I move the camera from landscape to portrait, it cuts the camera view size down. Is there a way to check the device orientation and change the frame bounds? Right now the line of code I'm using is
previewLayer?.frame = self.view.bounds
You got several options for this. The main idea is to get a function to be called whenever there is a change in the view.
Option 1, iOS8+
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.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
var previewLayer:CALayer?
if let layer = previewLayer {
layer.frame = self.view.bounds //or:
layer.frame.size = size //If coordinates is (x:0,y:0) you only need to update the size, and that is provided through the function
}
}
Option 2, iOS 6 & 7
In iOS 6 and iOS 7, your app supports the interface orientations defined in your app’s Info.plist file. A view controller can override the supportedInterfaceOrientations method to limit the list of supported orientations. Typically, the system calls this method only on the root view controller of the window or a view controller presented to fill the entire screen; child view controllers use the portion of the window provided for them by their parent view controller and no longer participate directly in decisions about what rotations are supported. The intersection of the app's orientation mask and the view controller's orientation mask is used to determine which orientations a view controller can be rotated into.
When a rotation occurs for a visible view controller, the willRotate(to:duration:), willAnimateRotation(to:duration:), and didRotate(from:) methods are called during the rotation. The viewWillLayoutSubviews() method is also called after the view is resized and positioned by its parent. If a view controller is not visible when an orientation change occurs, then the rotation methods are never called. However, the viewWillLayoutSubviews() method is called when the view becomes visible. Your implementation of this method can call the statusBarOrientation method to determine the device orientation.
When the orientation changes this method will be called and therefor you can update previewLayer.frame there.
override func viewDidLayoutSubviews() {
var previewLayer:CALayer?
if let layer = previewLayer {
layer.frame = self.view.bounds
}
}
or
override func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
var previewLayer:CALayer?
if let layer = previewLayer {
layer.frame = self.view.bounds
}
}

Disable auto rotation

I am new to Xcode and Swift. (Xcode 7.2, Swift 2.0) My first project contains a navigation controller and 3 other view controllers: A, B and C. I do not want any views to auto rotate. I want A and B permanently in portrait and C permanently in landscape. In General -> Deployment Info -> Device Orientation I have checked Portrait and Landscape left. In each VC I have code like this in viewDidLoad.
let value = UIInterfaceOrientation.LandscapeLeft.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
and the following method…
override func shouldAutorotate() -> Bool {
return false
}
Each view starts out in the desired orientation. However the darn thing keeps auto rotating.
You can't do what you're describing. Different view controllers at different depths in a navigation controller cannot have different fixed orientations. It isn't supported.
The only way to give a view controller a different fixed orientation from the previous view controller is as a presented view controller, not a view controller pushed onto a navigation controller's stack.
Also, this line:
UIDevice.currentDevice().setValue(value, forKey: "orientation")
would likely get your app rejected at the App Store. You are not allowed to set the device's orientation like this.
First way : If you don't want to rotate any VC in your project just uncheck the Device Orientation properties in your Targets window.
Second way: If you are using UINavigationController and if you want to call shouldAutorotate() function you should create a custom UINavigationController class and import the following code,
Like this, in your customNavigationController class,
override func shouldAutorotate() -> Bool {
return false
}
In this way you can change to another NavigationController's viewControllers to allow oriantations

iOS - unexpected rotation back to portrait on segue

first time posting, so forgive me if I'm not giving the right info in my question...
I'm creating an app (in swift), and I want to support all screen orientations for all screens (and have set in Xcode general tab accordingly). There's a login / launch screen, and then the root controller for the rest of the app is a UINavigationController.
My problem - The screen keeps rotating back to portrait on any segue, even though the device is in landscape orientation, and the screens all support landscape! Is this the standard behaviour on a segue? And if so, can I prevent it somehow?
To be clear - I just want the screen rotation to continue to reflect the device orientation following a segue - and all screens currently support all orientations.
I've tried setting shouldAutorotate to return false for a given screen / view controller, and extended UINavigationController to refer to the visible view controller's shouldautorotate() function, as follows:
extension UINavigationController {
public override func shouldAutorotate() -> Bool {
return visibleViewController!.shouldAutorotate()
}
}
This prevents the rotation away from the (landscape) device orientation on segue, but of course then if the user rotates back to portrait the screen remains landscape...
It seems like this should be really straightforward, but I couldn't find any info or other questions on it, just questions about restricting allowed orientations (I just want the screen orientation to reflect the device orientation at all times)...
One thought - is there a way to detect whether the shouldAutorotate function is being called following a segue? And return false in this instance, but true otherwise?
Any help would be gratefully received!
Thanks
Dan
Add these 2 methods into your view controller:
override func shouldAutorotate() -> Bool {
return false
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}

How can I tell programmatically if a ViewController presented UIModalPresentationFormSheet opened on full screen or not?

Modally presenting a ViewController on iPhone 6+ with a style of UIModalPresentationFormSheet opens differently according to orientation.
In portrait mode it seem like regular Modal (same as smaller iPhones).
But in landscape mode it opens as form (similar to iPads).
How can I tell programmatically what state was actually used (anywhere with in the VC's life cycle).
The controller displays as a form sheet when the size class for the controller has regular width.
So on an iPhone 6+ in landscape, or on an iPad in any orientation the horizontal size class is regular and the form is displayed less than full screen width.
You can test for this in a controller using:
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular) {
// ... Its showing as per the form specification
}
else{
// ... Its showing as a modal full screen.
}
Replace self with the variable for a controller if being called from somewhere else.
This also covers the case where you might use a popover, since when you use a popover on an iPad the size class changes to compact within the popover itself.
A clean Swift solution is below. A UIViewController extension that adds the isBeingPresentedInFormSheet method.
extension UIViewController {
func isBeingPresentedInFormSheet() -> Bool {
if let presentingViewController = presentingViewController {
return traitCollection.horizontalSizeClass == .Compact && presentingViewController.traitCollection.horizontalSizeClass == .Regular
}
return false
}
}
This method returns true if the view controller is currently being displayed in a Form Sheet.
In my experience this occurs when
modalPresentationStyle = .FormSheet
and the device is an iPad or iPhone 6 Plus in landscape orientation.

Force first screen of ios application to be portrait but other screens landscape ios 6+

I used this code for forcing my Home screen (first screen of my application) be portrait while other screens remain supporting all orientations:
public class RltNavigationController : UINavigationController
{
public RltNavigationController () : base ()
{
}
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations ()
{
if(this.TopViewController is HomeScreen )
return UIInterfaceOrientationMask.Portrait ;
else
return UIInterfaceOrientationMask.AllButUpsideDown ;
}
public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
{
// Return true for supported orientations
if(this.TopViewController is HomeScreen )
return (toInterfaceOrientation == UIInterfaceOrientation.Portrait );
else
return (toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown) ;
}
}
Now, suppose that the device is on the landscape orientation at home screen (Device is lanscape but screen just show portrait). Now if user go to other views, other views now show portrait while it should show landscape. What solution I can choose in order to load second views with theirs actual rotation?
EDIT
Thanks for all answers, Just notice that already the problem is not that I can not force the screen to be portrait. For understanding the problem please follow the scenario:
-> First screen forced to be portrait.
-> Device is landscape right and I'm in home screen(so home screen show portrait)
-> Now I switch to another screen that support all orientation
-> at another screen because the parent screen was portrait it show portrait (while because device is landscape it should show landscape)
You can also directly select from the XIB a particular viewController be Landscape or Portrait and the same loads.
You can not explicitly say, viewController be landscape and the view will be landscape. The way it works is, you ask the controller that is controller the screen, this may be a navigation controller, tab view controller, a modal, how they want to be able to rotate. If you have a navigation controller then all viewController will only have the rotation of your navigation controller.
There were a few tricks like subclassing the navigation controller and over the should auto rotate method, call [self.visibleViewController shouldAutoRotate]; which works for making screens rotate and not rotate, but if you have only 1 screen that supports all orientations and all the others do not, then you have a pushing/popping error where if you push or pop while in that different orientation the next viewController will be in that orientation.
Since you can't directly tell the rootViewController to explicitly rotate, the next best solutions are,
A: Use QuartzCore to manually rotate the view yourself
B: have a separate xib file for each orientation so when you rotate to landscape you see the landscape viewController and vice versa
The easiest way to do this is to create a custom navigation controller (subclass of UINavigationController) that inherits its rotation based on the currently visible view controller
Inside your custom navigation controller:
-(NSUInteger) supportedInterfaceOrientations {
return UIInterfaceOrientationMaskAllButUpsideDown;
}
-(BOOL) shouldAutorotate {
return self.topViewController.shouldAutorotate;
}
Then inside any of the view controllers within that, add these methods:
-(BOOL)shouldAutorotate{
return NO;
}
-(NSUInteger)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskPortrait;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationPortrait;
}
That's for a view you DON'T want to autorotate. Modify that accordingly for views you do want to rotate.
Make sure your project settings have rotation in the orientations you want enabled, and make sure to use your custom navigation controller instead of the regular one for any view hierarchies that contain multiple possible rotations.
Note that you may run into problems if a view that is rotated is popped and the previous view is not rotatable. Off the top of my head, I'm not sure whether this will work properly.

Resources