Detecting Barcodes on an image with the Vision Framework

Detecting Barcodes on an image with the Vision Framework

Use the new barcode detection API from the Vision framework to identify barcodes on images.

In the reference Reading QR codes and barcodes with the Vision framework we explored how to use VNDetectBarcodeRequest to retrieve data from various types of barcodes. With iOS 18, Apple introduced DetectBarcodesRequest, simplifying barcode detection in an image.

Let's see how to transition from using VNDetectBarcodeRequest to DetectBarcodeRequest with just a few modifications.

Here is the ScannerView implementation, responsible for providing access to the camera, performing the barcode detection, and returning the result of reading the barcode to its parent view through a binding.

Focus on the method detectBarcode(in:) in the Coordinator class.

import SwiftUI
import Vision
import AVFoundation

struct ScannerView: UIViewControllerRepresentable {
    
    @Binding var scannedString: String
    
    let captureSession = AVCaptureSession()
    
    func makeUIViewController(context: Context) -> UIViewController {
        let viewController = UIViewController()
        
        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
              let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
              captureSession.canAddInput(videoInput) else { return viewController }
        
        captureSession.addInput(videoInput)
        
        let videoOutput = AVCaptureVideoDataOutput()
        
        if captureSession.canAddOutput(videoOutput) {
            videoOutput.setSampleBufferDelegate(context.coordinator, queue: DispatchQueue(label: "videoQueue"))
            captureSession.addOutput(videoOutput)
        }
        
        let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = viewController.view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        viewController.view.layer.addSublayer(previewLayer)
        
        // Start capture session on a background thread
        
        Task(priority: .userInitiated) {
            self.captureSession.startRunning()
        }
        
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // Nothing to do here.
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
        var parent: ScannerView
        
        init(_ parent: ScannerView) {
            self.parent = parent
        }
        
        func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
            guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
            
            // Offload barcode detection to a background task
            Task {
                await self.detectBarcode(in: pixelBuffer)
            }
        }
        
        func detectBarcode(in pixelBuffer: CVPixelBuffer) async {
            var request = DetectBarcodesRequest()
            request.symbologies = [.qr, .ean13, .code128]
            
            do {
                let results = try await request.perform(on: pixelBuffer)
                if let observation = results.first?.payloadString {
                    Task { @MainActor in
                        self.parent.scannedString = observation
                    }
                }
            } catch {
                print("Error processing frame: \(error.localizedDescription)")
            }
        }
    }
}

The primary change is switching from using VNDetectBarcodesRequest to DetectBarcodesRequest. This new API is designed to be fully asynchronous and leverages Swift concurrency.

The detectBarcode(in:) method is now marked as async, so when analyzing the buffer we use await request.perform(on: pixelBuffer). This ensures barcode detection runs without blocking the main thread.

Since the results are retrieved asynchronously, we assign the payloadString value from the resulting observation to the binding property scannedString of the ScannerView, on the MainActor since it updates the user interface.

Here is an example of how to use it:

import SwiftUI

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

The DetectBarcodeRequest API is only available on iOS 18 and later. If you need backward compatibility, consider conditionally using VNDetectBarcodeRequest for earlier versions. This new API aligns with Swift 6’s concurrency model, preparing your code for a more robust and error-free future.