Custom camera view overlay size problems when app rotates - ios

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

Related

Mac Catalyst popover set/limit size problem

Is it possible to limit/fix (min/max) popover size on Mac Catalyst? See the attached video.
Example video
Yes, but it's a bit of a hack. Big Sur is loading these presented view controllers as their own windows, so we can grab the window's windowScene and set its sizeRestrictions. The best (?) place to do this is in the presented view controller's viewWillLayoutSubviews method:
class MyPresentedViewController: UIViewController {
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if #available(macCatalyst 14, *) {
view.window?.windowScene?.sizeRestrictions?.minimumSize = CGSize(width: 500, height: 500)
view.window?.windowScene?.sizeRestrictions?.maximumSize = CGSize(width: 800, height: 800)
}
}
}
If you don’t want the presented view to be resizable at all, just set the minimumSize and maximumSize to the same value.
I don't love using viewWillLayoutSubviews like this, but the windowScene is still nil in viewDidLoad and viewWillAppear, and while it is non-nil in viewDidAppear, setting sizeRestrictions there will cause a visible resize on screen.
The good news is that this problem may be fixed in Big Sur 11.1. According to the beta release notes, macOS 11.1 will respect preferredContentSize and they won't be resizable by default:
When you present a view controller with page sheet or form sheet presentation style, the size of the view controller’s root view is, by default, determined by the value returned from the presented view controller’s preferredContentSize method and the view is not resizable. You can arrange for the presented view controller to be resizable by using Auto Layout to specify the maximum and minimum sizes of its root view. To enable this, set the canResizeToFitContent property of the application’s main window to YES. One way to do this is to override the willMove(toWindow:) or didMoveToWindow() methods of a view in the main view controller. (65254666)

I want to run a particular viewcontroller in regular height trait environment in iOS Swift

I have many View Controllers in my ios App and they all support rotation but for one of my scenes (and only one), I would like to detect the trait environment and run it only if the device trait environment is regular height (so an iPhone in portrait or an iPad in both portrait or landscape). How to achieve this?
So I want to get the vertical size class and based on which I want to freeze the orientation. So if verticalSizeClass == .compact, the orientation of views of the UIViewController should be portrait else the orientation of the views of the UIViewController can be the same as that of the screen.
I am using the following code
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
let tc = UIScreen.main.traitCollection
let orientation: UIInterfaceOrientation = UIApplication.shared.statusBarOrientation
if tc.verticalSizeClass == .compact {
return .portrait
} else {
return orientation
}
}
But it's giving error messages for autolayout. I am using Xcode 10.2, running Swift 5.0.
There is a question How to force view controller orientation in iOS 8?
but it is from objective c perspective and more importantly I want the viewController to rotate as per the device, while the question "How to force view controller orientation in iOS 8?" locks the orientation to portrait or landscape and does not care about landscape left or right.
You have correctly used the size class to detect the trait environment. But try overriding this stored property of UIViewController instead.
override var supportedInterfaceOrientations: UIInterfaceOrientationMask{
get{
if .compact == UIScreen.main.traitCollection.verticalSizeClass{
return .portrait
}
return .all
}
}
When the user changes the device orientation, the system calls this
method on the root view controller or the topmost presented view
controller that fills the window. If the view controller supports the
new orientation, the window and view controller are rotated to the new
orientation. This method is only called if the view controller's
shouldAutorotate method returns true.
Reference Documentation
as I understand you want to get the height of specific Scene
let screenSize: CGRect = UIScreen.main.bounds
let screenWidth = screenSize.width
let screenHeight = screenSize.height
print("Screen width = \(screenWidth), screen height = \(screenHeight)")
this gives you the height and the width.

Is there a way to have a single view not rotate?

I am trying to create a view that doesn't rotate with device orientation. I want just this one view to not rotate because I have a UIToolbar that I don't want to rotate. Is this possible? If so, how do I implement it? I am using Swift. Also, is it possible to rotate the UIBarButtonItems with device orientation on the UIToolbar? If so, how do I implement that? Just to restate: I want the view and toolbar within it to not rotate with orientation; I want the buttons on the toolbar to rotate with orientation. Thanks.
Per the View Controller Programing Guide
If you want to temporarily disable automatic rotation, avoid manipulating the orientation masks to do this. Instead, override the shouldAutorotate variable on the initial view controller. This method is called before performing any autorotation. If it returns NO, then the rotation is suppressed.
So you need to subclass 'UINavigationController', implement shouldAutorotate and use your navigation controller class in your storyboard.
Swift 3
override var shouldAutorotate: Bool {
let currentViewController = self.topViewController
return !currentViewController.isKind(of: DetailViewController.classForCoder())
}
Why not just rotate the view in question -90 degrees or +90 degrees as required? I'll happily provide example code if you'd like.
Edit:
I was suggesting something like this (but look at the caveat after the code):
let degrees = 90.0 // or -90, 180 depending on phone's movement.
let rotatedView: UIView = <yourOriginalView>
rotatedView.transform = CGAffineTransform = CGAffineTransform(rotationAngle: degrees * CGFloat(M_PI / 180))
In iOS 8.0 and later, the transform property does not affect Auto Layout. Auto layout calculates a view’s alignment rectangle based on its untransformed frame. -- So this might not work for your purposes if you're using autolayout.... And I admit this is a kludge. 😉🙄😉
I see two possibilities, and prefer the second because it doesn't rotate your views twice. I will use the term static view for the single view that you don't want to rotate (= the UIToolbar).
Possibility 1
Subscribe to device orientation changes. Then, when the orientation changes, and the UI rotates, rotate the static view in the opposite direction.
For example when you get a notification that the device is rotated 90° clockwise:
The whole user interface (including the static view) will rotate 90° counterclockwise automatically
Rotate the static view 90° clockwise, to cancel the automatic rotation behaviour.
Possibility 2
Disable the automatic rotation behaviour and then manually rotate all views except the one you don't want to rotate. To rotate multiple views at once: put them in a custom view and rotate that custom view. Here is a step-by-step guide:
Disable automatic rotation in your view controller. Do this by overriding the ViewController's shouldAutoRotate variable.
In interface builder: put everything except the static view in a custom UIView.
Observe device orientation changes and rotate the statusbar and your custom view (you created in step 2) when the device's orientation changes.
An example implementation in swift can be found at: GitHub/Dev1an/Static iOS toolbar
Rotating views
For the question "how to rotate views" have a look at: https://stackoverflow.com/a/28717635/2616297
Straightforward. You should simply override the shouldRotate variable and set it to false for the VC you wish to prevent rotation in.
UPDATED - Swift 3
override var shouldAutorotate: Bool {
return false
}
I used the information from all of the answers here, as well as from this post overriding shouldAutorotate not working in Swift 3, to find the answer to my question. I created a MainNavigationController class within my MainViewController.swift file and then overrided shouldAutorotate like so:
import UIKit
class MainNavigationController: UINavigationController {
override var shouldAutorotate: Bool {
return false }
}
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
}
The last step was to go into my Main.storyboard file, click on the navigation controller for the view, and change its class to MainNavigationController. Once I did that, it worked perfectly. Thanks to all the people who answered this question.

Size a UIViewController's view to parent UIWindow's bounds

I want to give a UIViewController's view a size that is different from the device's screen size. I know I can usually achieve this by adding the view controller as a child view controller of another parent UIViewController that has defined a frame for the child, but I am in a situation that seems a little different.
I have a UIWindow that only takes up a portion of the screen (it's got a frame that's basically (0, 0, DEVICE_WIDTH, HEIGHT_LESS_THAN_DEVICE_HEIGHT). This window shows up with the proper sizing and positioning when presented. I am setting a view controller as the rootViewController of the window, and then presenting the window by setting its hidden value to false. When this happens, the view controller's view ends up sized to fill the device's screen (i.e. a frame of (0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)).
What I would like is for the view controller to inherit its sizing from the UIWindow it is set as the root view controller of. Is there a way to do this?
I have also tried overriding loadView() and returning a custom-sized view there. Logging the view shows that the view controller's view object is correctly sized during viewDidLoad, but is overwritten with the default size by viewWillAppear:. I would be open to using loadView() to size the view controller if inheriting sizing from the window isn't possible, but I don't know how to make the custom size stick.
Note: The reason why I am trying to add a view controller to the window is because I want to take advantage of the view controller lifecycle methods such as viewDidAppear:, which is why I am not just creating a simple UIView and adding it as a subview of the window.
As counter intuitive as it may seem, if you set set self.view.frame on viewWillAppear (IOS 8) or viewDidAppear (IOS 7) you will be able to make it work.
Swift code (IOS 8):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Banner style size, for example
self.view.frame = CGRectMake(0, 0, 320, 50)
}
For IOS 7, I had to use viewDidAppear, which is obviously an unsatisfactory solution. So I had to start the view with alpha = 0.0 and set alpha = 1.0 on viewDidAppear, after modifying self.view.frame.
Hope it helps.

Force iOS view to not rotate, while still allowing child to rotate

I have a view controller with a child view controller.
tab bar controller
|
|
nav controller
|
|
UIPageViewController (should rotate)
|
|
A (Video Player) (shouldn't rotate)
|
|
B (Controls overlay) (should rotate)
A should be forced to stay portrait at all times, but B should be allowed to rotate freely.
I know shouldAutorotate applies to any view controllers and its children, but is there any way to get around this? It seems like I could use shouldAutorotateToInterfaceOrientation, but this is blocked in iOS 8.
I'd like to keep a video player static (so horizontal videos are always horizontal regardless of device orientation), while the controls layer subview overlay is allowed to freely rotate.
I'm using Swift.
I had this exact problem, and found out quickly there's a lot of bad advice floating around about autorotation, especially because iOS 8 handles it differently than previous versions.
First of all, you don't want to apply a counterrotation manually or subscribe to UIDevice orientation changes. Doing a counterrotation will still result in an unsightly animation, and device orientation isn't always the same as interface orientation. Ideally you want the camera preview to stay truly frozen, and your app UI to match the status bar orientation and size as they change, exactly like the native Camera app.
During an orientation change in iOS 8, the window itself rotates rather than the view(s) it contains. You can add the views of multiple view controllers to a single UIWindow, but only the rootViewController will get an opportunity to respond via shouldAutorotate(). Even though you make the rotation decision at the view controller level, it's the parent window that actually rotates, thus rotating all of its subviews (including ones from other view controllers).
The solution is two UIWindow stacked on top of each other, each rotating (or not) with its own root view controller. Most apps only have one, but there's no reason you can't have two and overlay them just like any other UIView subclass.
Here's a working proof-of-concept, which I've also put on GitHub here. Your particular case is a little more complicated because you have a stack of containing view controllers, but the basic idea is the same. I'll touch on some specific points below.
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var cameraWindow: UIWindow!
var interfaceWindow: UIWindow!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
let screenBounds = UIScreen.mainScreen().bounds
let inset: CGFloat = fabs(screenBounds.width - screenBounds.height)
cameraWindow = UIWindow(frame: screenBounds)
cameraWindow.rootViewController = CameraViewController()
cameraWindow.backgroundColor = UIColor.blackColor()
cameraWindow.hidden = false
interfaceWindow = UIWindow(frame: CGRectInset(screenBounds, -inset, -inset))
interfaceWindow.rootViewController = InterfaceViewController()
interfaceWindow.backgroundColor = UIColor.clearColor()
interfaceWindow.opaque = false
interfaceWindow.makeKeyAndVisible()
return true
}
}
Setting a negative inset on interfaceWindow makes it slightly larger than the screen bounds, effectively hiding the black rectangular mask you'd see otherwise. Normally you wouldn't notice because the mask rotates with the window, but since the camera window is fixed the mask becomes visible in the corners during rotation.
class CameraViewController: UIViewController {
override func shouldAutorotate() -> Bool {
return false
}
}
Exactly what you'd expect here, just add your own setup for AVCapturePreviewLayer.
class InterfaceViewController: UIViewController {
var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
contentView = UIView(frame: CGRectZero)
contentView.backgroundColor = UIColor.clearColor()
contentView.opaque = false
view.backgroundColor = UIColor.clearColor()
view.opaque = false
view.addSubview(contentView)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let screenBounds = UIScreen.mainScreen().bounds
let offset: CGFloat = fabs(screenBounds.width - screenBounds.height)
view.frame = CGRectOffset(view.bounds, offset, offset)
contentView.frame = view.bounds
}
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.All.rawValue)
}
override func shouldAutorotate() -> Bool {
return true
}
}
The last trick is undoing the negative inset we applied to the window, which we achieve by offsetting view the same amount and treating contentView as the main view.
For your app, interfaceWindow.rootViewController would be your tab bar controller, which in turn contains a navigation controller, etc. All of these views need to be transparent when your camera controller appears so the camera window can show through beneath it. For performance reasons you might consider leaving them opaque and only setting everything to transparent when the camera is actually in use, and set the camera window to hidden when it's not (while also shutting down the capture session).
Sorry to post a novel; I haven't seen this addressed anywhere else and it took me a while to figure out, hopefully it helps you and anyone else who's trying to get the same behavior. Even Apple's AVCam sample app doesn't handle it quite right.
The example repo I posted also includes a version with the camera already set up. Good luck!
You can try this -
Objective -C code if you have its alternative in swift:
-(NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
if ()//Place your condition here like if A is visible
{
return UIInterfaceOrientationMaskPortrait;
}
return UIInterfaceOrientationMaskAll;
}
You can subscribe to rotation change notifications, and manually set the rotation transform matrix on the subview you want to rotate.
I'm not sure, but I think you could create an own class for your subview and override the shouldAutorotate method etc. That way it should override the shouldAutorotate from the parent-viewcontroller.
Short answer: No, all visible controllers and views rotate (or don't rotate) together.
Long answer:
First, you must implement autorotate decision functions in the root controller; that may mean making a nav controller subclass.
You can hack your desired behavior by having the parent view autorotate -- but have it manually rotate itself back to appear un-rotated.
Or, you can NOT autorotate, but listen for notifications that the physical device rotated, and manually rotate whatever views you want to, eg: Replicate camera app rotation to landscape IOS 6 iPhone
Also see, fyi:
How to force a UIViewController to Portrait orientation in iOS 6
shouldAutoRotate Method Not Called in iOS6
iOS6: supportedInterfaceOrientations not working (is invoked but the interface still rotates)
How to implement UIViewController rotation in response to orientation changes?
The simplest, most straight-forward answer to this question is to look at Apple's AVCam sample code. The key parts for me were that it:
Uses a view whose layerClass is AVCaptureVideoPreviewLayer.
Sets the videoOrientation of the AVCaptureVideoPreviewLayer's connection to match the application's statusBarOrientation when the view is presented, essentially viewWillAppear(_:).
Sets the videoOrientation to match UIDevice.currentDevice().orientation in viewWillTransitionToSize(_:withTransitionCoordinator:).
Enables autorotation and supports all interface orientations.
I implemented the background window approach described by jstn and it worked fairly well, but the reality is that it is much more complicated than is necessary. AVCam works great and has relatively simple approach.

Resources