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

