Integrating AVCaptureVideoPreviewLayer into SwiftUI using UIViewRepresentable...
Integrating AVCaptureVideoPreviewLayer into SwiftUI using UIViewRepresentable, with support for orientation handling, gesture recognition, and proper lifecycle management.
The bridge between UIKit and SwiftUI:
makeUIView(context:) - Creates the UIKit view onceupdateUIView(_:context:) - Called when SwiftUI state changesHandles UIKit delegate patterns and gesture recognition:
makeCoordinator()The modern videoRotationAngle property (CGFloat degrees) replaces deprecated videoOrientation:
import UIKit
import AVFoundation
class CameraPreviewUIView: UIView {
var previewLayer: AVCaptureVideoPreviewLayer? {
didSet {
oldValue?.removeFromSuperlayer()
if let layer = previewLayer {
self.layer.addSublayer(layer)
setNeedsLayout()
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
previewLayer?.frame = bounds
updatePreviewOrientation()
}
func updatePreviewOrientation() {
guard let connection = previewLayer?.connection else { return }
let interfaceOrientation = window?.windowScene?.interfaceOrientation ?? .portrait
let angle = interfaceOrientation.videoRotationAngle
if connection.isVideoRotationAngleSupported(angle) {
connection.videoRotationAngle = angle
}
}
}
extension UIInterfaceOrientation {
var videoRotationAngle: CGFloat {
switch self {
case .portrait: return 90
case .portraitUpsideDown: return 270
case .landscapeLeft: return 180
case .landscapeRight: return 0
default: return 90
}
}
}
import SwiftUI
import AVFoundation
struct CameraPreviewView: UIViewRepresentable {
let cameraManager: CameraManager
var onZoomChange: ((CGFloat, CGFloat) -> Void)?
func makeUIView(context: Context) -> CameraPreviewUIView {
let view = CameraPreviewUIView()
view.previewLayer = cameraManager.makePreviewLayer()
// Add pinch gesture targeting the coordinator
let pinchGesture = UIPinchGestureRecognizer(
target: context.coordinator,
action: #selector(Coordinator.handlePinch(_:))
)
view.addGestureRecognizer(pinchGesture)
return view
}
func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {
uiView.updatePreviewOrientation()
context.coordinator.onZoomChange = onZoomChange
}
func makeCoordinator() -> Coordinator {
Coordinator(cameraManager: cameraManager, onZoomChange: onZoomChange)
}
}
extension CameraPreviewView {
class Coordinator: NSObject {
let cameraManager: CameraManager
private var baseZoomFactor: CGFloat = 1.0
var onZoomChange: ((CGFloat, CGFloat) -> Void)?
init(cameraManager: CameraManager, onZoomChange: ((CGFloat, CGFloat) -> Void)?) {
self.cameraManager = cameraManager
self.onZoomChange = onZoomChange
}
@MainActor @objc func handlePinch(_ gesture: UIPinchGestureRecognizer) {
switch gesture.state {
case .began:
baseZoomFactor = cameraManager.currentZoomFactor
case .changed:
cameraManager.applyZoomDelta(
scale: gesture.scale,
baseZoom: baseZoomFactor
)
onZoomChange?(
cameraManager.currentZoomFactor,
cameraManager.maxZoomFactor
)
case .ended, .cancelled:
cameraManager.finalizeZoom()
default:
break
}
}
}
}
struct CameraScreen: View {
@StateObject private var cameraManager = CameraManager()
@State private var currentZoom: CGFloat = 1.0
var body: some View {
ZStack {
CameraPreviewView(
cameraManager: cameraManager,
onZoomChange: { current, max in
currentZoom = current
}
)
.ignoresSafeArea()
VStack {
Spacer()
Text(String(format: "%.1fx", currentZoom))
.padding()
.background(.ultraThinMaterial)
.cornerRadius(8)
}
}
.onAppear { cameraManager.startSession() }
.onDisappear { cameraManager.stopSession() }
}
}
Problem: Preview layer doesn't resize with the view.
Solution: Update previewLayer.frame = bounds in layoutSubviews().
Problem: Gesture recognizer stops working after view updates. Solution: Use the Coordinator as the gesture target—it persists across updates.
Problem: Preview doesn't rotate with device.
Solution: Call updatePreviewOrientation() in both layoutSubviews() AND updateUIView().
Problem: Crashes from wrong thread access.
Solution: Mark gesture handlers with @MainActor.
Problem: videoOrientation property deprecated in iOS 17.
Solution: Use videoRotationAngle with isVideoRotationAngleSupported(_:) check.
Problem: Crashes when setting zoom factor outside valid range.
Solution: Clamp zoom between 1.0 and device.maxAvailableVideoZoomFactor.
| Aspect | Best Practice |
|---|---|
| Layer Management | Custom UIView subclass with didSet observer |
| Frame Updates | Override layoutSubviews(), update layer frame |
| Gesture Handling | Coordinator as target, @MainActor annotations |
| Orientation | Use videoRotationAngle (iOS 17+) with support check |
| Thread Safety | @MainActor on all UI-touching methods |
Derived from CarSeet project - CameraPreviewView.swift