I have a simple camera preview implementation:
import SwiftUI
import AVFoundation
struct CameraView: View {
@StateObject var model = CameraModel()
var body: some View {
CameraPreview(camera: model)
.safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
Color.clear
.frame(height: 0)
.background(Material.bar)
}
.ignoresSafeArea(.all, edges: .top)
.onAppear() {
model.check()
}
}
}
struct CameraPreview: UIViewRepresentable {
@ObservedObject var camera: CameraModel
func makeUIView(context: Context) -> some UIView {
let view = UIView(frame: UIScreen.main.bounds)
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
camera.preview.frame = view.frame
view.layer.addSublayer(camera.preview)
camera.start()
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct CameraView_Previews: PreviewProvider {
static var previews: some View {
CameraView()
}
}
class CameraModel: ObservableObject {
@Published var session = AVCaptureSession()
@Published var alert = false
@Published var preview: AVCaptureVideoPreviewLayer!
func check() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setUp()
break
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { (status) in
if status {
self.setUp()
}
}
break
case .denied:
self.alert.toggle()
break
default:
break
}
}
func setUp() {
do {
self.session.beginConfiguration()
let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
let input = try AVCaptureDeviceInput(device: device!)
if self.session.canAddInput(input) {
self.session.addInput(input)
}
self.session.commitConfiguration()
}
catch {
print(error.localizedDescription)
}
}
func start() {
self.session.startRunning()
}
}
The problem is that it doesn't handle screen rotations:
I found similar topics, for example, this one, but I am a noobie in iOS development, I can't even understand where to put this solution. I've checked neither View
, nor UIViewRepresentable
have such methods to override.
How to handle screen rotation in AVCaptureVideoPreviewLayer
?
CodePudding user response:
If you want to update layer frame in rotation, you need to create custom UIView
and override layoutSubviews()
. Inside layoutSubviews()
, you need to update frame for sublayers.
The code will be as below.
struct CameraPreview: UIViewRepresentable {
@ObservedObject var camera: CameraModel
class LayerView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
// To disable default animation of layer. You can comment out those lines with `CATransaction` if you want to include
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.sublayers?.forEach({ layer in
layer.frame = frame
})
CATransaction.commit()
}
}
func makeUIView(context: Context) -> some UIView {
let view = LayerView()
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
view.layer.addSublayer(camera.preview)
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
CodePudding user response:
This is a working variant with video rotation based on Dscyre Scotti'es answer:
struct CameraView: View {
@StateObject var model = CameraModel()
var body: some View {
CameraPreview(camera: model)
.safeAreaInset(edge: .bottom, alignment: .center, spacing: 0) {
Color.clear
.frame(height: 0)
.background(Material.bar)
}
.ignoresSafeArea(.all, edges: [.top, .horizontal])
.onAppear() {
model.check()
}
}
}
struct CameraPreview: UIViewRepresentable {
@ObservedObject var camera: CameraModel
class LayerView: UIView {
var parent: CameraPreview! = nil
override func layoutSubviews() {
super.layoutSubviews()
// To disable default animation of layer. You can comment out those lines with `CATransaction` if you want to include
CATransaction.begin()
CATransaction.setDisableActions(true)
layer.sublayers?.forEach({ layer in
layer.frame = UIScreen.main.bounds
})
self.parent.camera.rotate(orientation: UIDevice.current.orientation)
CATransaction.commit()
}
}
func makeUIView(context: Context) -> some UIView {
let view = LayerView()
view.parent = self
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.videoGravity = AVLayerVideoGravity.resizeAspectFill
camera.preview.frame = view.frame
view.layer.addSublayer(camera.preview)
camera.start()
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
struct CameraView_Previews: PreviewProvider {
static var previews: some View {
CameraView()
}
}
class CameraModel: ObservableObject {
@Published var session = AVCaptureSession()
@Published var alert = false
@Published var preview: AVCaptureVideoPreviewLayer!
func check() {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setUp()
break
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { (status) in
if status {
self.setUp()
}
}
break
case .denied:
self.alert.toggle()
break
default:
break
}
}
func setUp() {
do {
self.session.beginConfiguration()
let device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
let input = try AVCaptureDeviceInput(device: device!)
if self.session.canAddInput(input) {
self.session.addInput(input)
}
self.session.commitConfiguration()
}
catch {
print(error.localizedDescription)
}
}
func start() {
self.session.startRunning()
}
func rotate(orientation: UIDeviceOrientation) {
let videoConnection = self.preview.connection
switch orientation {
case .portraitUpsideDown:
videoConnection?.videoOrientation = .portraitUpsideDown
case .landscapeLeft:
videoConnection?.videoOrientation = .landscapeRight
case .landscapeRight:
videoConnection?.videoOrientation = .landscapeLeft
case .faceDown:
videoConnection?.videoOrientation = .portraitUpsideDown
default:
videoConnection?.videoOrientation = .portrait
}
}
}