Safely use AVCaptureSession + Swift 6.2 Concurrency

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?