I have two view controllers. One is the root VC and contains the UI interface such as the record button. On this view controller, I also display the view of another VC at in
You'll find that the Camera app only supports Portrait orientation and rotates view elements as required.
I came here with a similar issue, except I ended up requiring the AVCaptureVideoPreviewLayer
to stay oriented with the rest of the view so i could correctly use the metadataOutputRectConverted(fromLayerRect:)
method.
Like Erik Allens answer above, My solution is based off Technical Q&A QA1890 Preventing a View From Rotating, but has been updated to Swift 5 and removes the rotation transform once the interface transition is complete.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.cameraPreviewView = UIView(frame: view.bounds)
self.cameraPreviewView.layer.addSublayer(self.videoPreviewLayer)
self.videoPreviewLayer.connection?.videoOrientation = UIApplication.shared.statusBarOrientation.asAVCaptureVideoOrientation()
self.videoPreviewLayer.frame = self.cameraPreviewView.bounds
self.view.addSubview(self.cameraPreviewView)
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
self.cameraPreviewView.center = self.view.bounds.midPoint
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
let cameraPreviewTransform = self.cameraPreviewView.transform
coordinator.animate(alongsideTransition: { context in
// Keep the camera preview view inversely rotatated with the rest of the view during transition animations
let deltaTransform = coordinator.targetTransform
let deltaAngle: CGFloat = atan2(deltaTransform.b, deltaTransform.a)
var previewCurrentRotation = atan2(cameraPreviewTransform.b, cameraPreviewTransform.a)
// Adding a small value to the rotation angle forces the animation to occur in a the desired direction,
// preventing an issue where the view would appear to rotate 2PI radians during a rotation from LandscapeRight -> LandscapeLeft.
previewCurrentRotation += -1 * deltaAngle + 0.0001
self.cameraPreviewView.layer.setValue(previewCurrentRotation, forKeyPath: "transform.rotation.z")
}, completion: { context in
// Now the view transition animations are complete, we will adjust videoPreviewLayer properties to fit the current orientation
// Changing the frame of a videoPreviewLayer animates the resizing of the preview view, so we disable animations (actions) to remove this effect
CATransaction.begin()
CATransaction.setDisableActions(true)
self.cameraPreviewView.transform = cameraPreviewTransform
self.cameraPreviewView.frame = self.view.bounds
self.videoPreviewLayer.connection?.videoOrientation = UIApplication.shared.statusBarOrientation.asAVCaptureVideoOrientation()
self.videoPreviewLayer.frame = self.cameraPreviewView.bounds
CATransaction.commit()
})
}
extension UIInterfaceOrientation {
func asAVCaptureVideoOrientation() -> AVCaptureVideoOrientation {
switch self {
case .portrait:
return .portrait
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
case .portraitUpsideDown:
return .portraitUpsideDown
default:
return .portrait
}
}
}
On the view that shows the camera output add:
AVCaptureVideoPreviewLayer *layer = (AVCaptureVideoPreviewLayer *)self.layer;
if ([layer.connection isVideoOrientationSupported]) {
[layer.connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
}
AVCaptureVideoOrientationPortrait is just one option. You can choose from the following:
typedef NS_ENUM(NSInteger, AVCaptureVideoOrientation) {
AVCaptureVideoOrientationPortrait = 1,
AVCaptureVideoOrientationPortraitUpsideDown = 2,
AVCaptureVideoOrientationLandscapeRight = 3,
AVCaptureVideoOrientationLandscapeLeft = 4,
}
This must be done after you setup the session.
Make sure to set shouldAutorotate to return false:
-(BOOL)shouldAutorotate{
return NO;
}
register for Notifications that orientation changed:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(orientationChanged:)
name:UIDeviceOrientationDidChangeNotification
object:nil];
implement the notification change
-(void)orientationChanged:(NSNotification *)notif {
UIDeviceOrientation deviceOrientation = [[UIDevice currentDevice] orientation];
// Calculate rotation angle
CGFloat angle;
switch (deviceOrientation) {
case UIDeviceOrientationPortraitUpsideDown:
angle = M_PI;
break;
case UIDeviceOrientationLandscapeLeft:
angle = M_PI_2;
break;
case UIDeviceOrientationLandscapeRight:
angle = - M_PI_2;
break;
default:
angle = 0;
break;
}
}
and rotate the UI
[UIView animateWithDuration:.3 animations:^{
self.closeButton.transform = CGAffineTransformMakeRotation(angle);
self.gridButton.transform = CGAffineTransformMakeRotation(angle);
self.flashButton.transform = CGAffineTransformMakeRotation(angle);
} completion:^(BOOL finished) {
}];
This is how I implement the screen being locked but rotating the UI, if this works link the stacks post and I can copy it over there and you can tick it :P
I have a very similar situation. I just have one view controller and I want to have a AVCaptureVideoPreviewLayer
that doesn't rotate in it. I found the accepted solution by @SeanLintern88 did not work for me; the status bar never moved and the WKWebView I had on the screen was not getting resizes properly.
One of the bigger issues I ran into was that I was putting my AVCaptureVideoPreviewLayer
in the view controller's view. It is much better to create a new UIView
just to hold the layer.
After that I found a technical note from Apple QA1890: Preventing a View From Rotating. This allowed me to produce the following swift code:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
{
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
coordinator.animateAlongsideTransition(
{ (UIViewControllerTransitionCoordinatorContext) in
let deltaTransform = coordinator.targetTransform()
let deltaAngle = atan2f(Float(deltaTransform.b), Float(deltaTransform.a))
var currentRotation : Float = (self.previewView!.layer.valueForKeyPath("transform.rotation.z")?.floatValue)!
// Adding a small value to the rotation angle forces the animation to occur in a the desired direction, preventing an issue where the view would appear to rotate 2PI radians during a rotation from LandscapeRight -> LandscapeLeft.
currentRotation += -1 * deltaAngle + 0.0001;
self.previewView!.layer.setValue(currentRotation, forKeyPath: "transform.rotation.z")
self.previewView!.layer.frame = self.view.bounds
},
completion:
{ (UIViewControllerTransitionCoordinatorContext) in
// Integralize the transform to undo the extra 0.0001 added to the rotation angle.
var currentTransform : CGAffineTransform = self.previewView!.transform
currentTransform.a = round(currentTransform.a)
currentTransform.b = round(currentTransform.b)
currentTransform.c = round(currentTransform.c)
currentTransform.d = round(currentTransform.d)
self.previewView!.transform = currentTransform
})
}
The original tech note did not have the line self.previewView!.layer.frame = self.view.bounds
but I found that very necessary because although the anchor point doesn't move, the frame has. Without that line, the preview will be offset.
Also, since I am doing all of the work keeping the view in the correct position, I had to remove all the positioning constraints on it. When I had them in, they would cause the preview to instead be offset in the opposite direction.