I would like to have a camera view show up in sheet view to be able to scan barcodes. For some reason, the camera layer created doesn't want to appear in the sheet view, even though the green dot appears on the iPhone, or the logs say that everything is fine.
// MainView.swift
@State private var showScanSheet = false
var body: some View {
NavigationStack {
VStack {
...
}.sheet(isPresented: $showScanSheet) {
ScannerView()
}
}
}
// ScannerView.swift
import SwiftUI
import AVKit
struct ScannerView: View {
@State private var isScanning: Bool = false
@State private var session: AVCaptureSession = .init()
@State private var cameraPermission: Permission = .idle
@State private var barcodeOutput: AVCaptureMetadataOutput = .init()
@State private var errorMessage: String = ""
@State private var showError: Bool = false
@Environment(\.openURL) private var openURL
@StateObject private var barcodeDelegate = BarcodeScannerDelegate()
var body: some View {
GeometryReader {
let size = $0.size
ZStack {
CameraView(frameSize: CGSize(width: size.width, height: 200), session: $session).scaleEffect(0.97)
RoundedRectangle(cornerRadius: 10, style: .circular)
.trim(from: 0.55, to: 0.60)
.stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.padding()
RoundedRectangle(cornerRadius: 10, style: .circular)
.trim(from: 0.55, to: 0.60)
.stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.rotationEffect(.init(degrees: 180))
.padding()
RoundedRectangle(cornerRadius: 10, style: .circular)
.trim(from: 0.40, to: 0.45)
.stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.padding()
RoundedRectangle(cornerRadius: 10, style: .circular)
.trim(from: 0.40, to: 0.45)
.stroke(Color.red, style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.rotationEffect(.init(degrees: 180))
.padding()
}
.frame(width: size.width, height: 200)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.onAppear(perform: checkCameraPermission)
.alert(errorMessage, isPresented: $showError) {
if cameraPermission == .denied {
Button("Settings") {
let settingsString = UIApplication.openSettingsURLString
if let settingsURL = URL(string: settingsString) {
openURL(settingsURL)
}
}
Button("Cancel", role: .cancel) {}
}
}
}
func checkCameraPermission() {
print("Checking camera permission")
Task {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
cameraPermission = .approved
setupCamera()
case .notDetermined, .denied, .restricted:
if await AVCaptureDevice.requestAccess(for: .video) {
cameraPermission = .approved
setupCamera()
} else {
cameraPermission = .denied
presentError("Please provide access to the camera for scanning barcodes.")
}
default: break
}
print(cameraPermission)
}
}
func setupCamera() {
do {
guard let device = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera], mediaType: .video, position: .back).devices.first else {
presentError("Unknown error.")
return
}
let input = try AVCaptureDeviceInput(device: device)
guard session.canAddInput(input), session.canAddOutput(barcodeOutput) else {
presentError("Unknown error.")
return
}
session.beginConfiguration()
session.addInput(input)
session.addOutput(barcodeOutput)
barcodeOutput.metadataObjectTypes = [.upce, .ean8, .ean13]
barcodeOutput.setMetadataObjectsDelegate(barcodeDelegate, queue: .main)
session.commitConfiguration()
DispatchQueue.global(qos: .background).async {
session.startRunning()
}
} catch {
presentError(error.localizedDescription)
}
}
func presentError(_ message: String) {
errorMessage = message
showError.toggle()
}
}
// BarcodeScannerDelegate.swift
import Foundation
import AVKit
class BarcodeScannerDelegate: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
if let metadataObject = metadataObjects.first {
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
guard let scannedCode = readableObject.stringValue else { return }
print(scannedCode)
}
}
}
// CameraView.swift
import SwiftUI
import AVKit
struct CameraView: UIViewRepresentable {
var frameSize: CGSize
@Binding var session: AVCaptureSession
func makeUIView(context: Context) -> UIView {
let view = UIViewType(frame: CGRect(origin: .zero, size: frameSize))
view.backgroundColor = .clear
let cameraLayer = AVCaptureVideoPreviewLayer(session: session)
cameraLayer.frame = .init(origin: .zero, size: frameSize)
cameraLayer.videoGravity = .resizeAspectFill
cameraLayer.masksToBounds = true
view.layer.addSublayer(cameraLayer)
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
However, the app works completely as expected when instead of using a sheet
, I use a fullScreenCover
or a navigationDestination
in MainView.swift.
when using with fullScreenCover
Thank you for your help.
I encountered the same issue, I think we both followed Kavsoft :). After some investigation, I discovered the root cause.
The problem lies within the initialization of the camera layer size in the CameraView struct. The camera layer is initially created with a size of zero:
cameraLayer.frame = .init(origin: .zero, size: frameSize)
When the sheet view is presented, the frameSize is initialized as zero and later updated to the actual size. However, this update does not propagate to the camera layer, resulting in it not appearing correctly.
A quick fix would be to provide a fixed frameSize like CGSize(width: 300, height: 300), which makes the camera view visible. However, this approach is not optimal.
To address the issue properly, we need to ensure that the camera layer updates its size whenever there is a change in the view's dimensions. This can be achieved by implementing the updateUIView method. Here's how:
func updateUIView(_ uiView: UIViewType, context: Context) {
// Check if the view's size has changed
guard uiView.frame.size != frameSize else { return }
// Update the view's size
uiView.frame.size = frameSize
// Update the camera layer's frame accordingly
if let cameraLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
cameraLayer.frame = CGRect(origin: .zero, size: frameSize)
}
}
By incorporating this code snippet into the CameraView struct, the camera layer will adjust its size dynamically, ensuring that it appears correctly in the sheet view.
Hope it helps.