Ok so I found figured out how to make this compile and run. @calicoding runDeferedStartWhenNeeded()was a good jumping off point but the actual issue was just getting the threading/concurrency correct in the delegate methods and the AVCaptureSessionsetup. It seems super super hacky. Here’s the full solution I ended up on:
import SwiftUI
import Vision
@preconcurrency import AVFoundation
struct ContentView: View {
@State private var scannedString: String = "Scan a QR code or barcode"
var body: some View {
ZStack(alignment: .bottom) {
ScannerView(scannedString: $scannedString)
.edgesIgnoringSafeArea(.all)
Text(scannedString)
.padding()
.background(.ultraThinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding()
}
}
}
struct ScannerView: UIViewControllerRepresentable {
@Binding var scannedString: String
// Serial queue for all AVCaptureSession operations
private let sessionQueue = DispatchQueue(label: "sessionQueue")
// Separate queue for Vision barcode processing
private let videoQueue = DispatchQueue(label: "videoQueue")
let captureSession = AVCaptureSession()
func makeUIViewController(context: Context) -> UIViewController {
let viewController = UIViewController()
sessionQueue.async { [captureSession] in
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
captureSession.canAddInput(videoInput) else { return }
captureSession.addInput(videoInput)
let videoOutput = AVCaptureVideoDataOutput()
if captureSession.canAddOutput(videoOutput) {
Task { @MainActor in
videoOutput.setSampleBufferDelegate(context.coordinator, queue: self.videoQueue)
}
captureSession.addOutput(videoOutput)
}
captureSession.startRunning()
}
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
previewLayer.frame = viewController.view.bounds
previewLayer.videoGravity = .resizeAspectFill
viewController.view.layer.addSublayer(previewLayer)
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
var parent: ScannerView
init(_ parent: ScannerView) {
self.parent = parent
}
nonisolated func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
// Detect barcode here
}
}
}
I feel like there’s gotta be a better way to do all of this especially the setup wrapped inside sessionQueue.async. The entire time while figuring this out I was thinking there needs to be a way to put the initialization and starting of the capturing and even just other basic camera operations into an actor. I feel like @vns was onto something with his reply here.
@calicoding Happy this runs but thoughts on how I might be able to put this into an actor/clean this up a bit?