You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于Vision Framework的扫码器需求实现:点击按钮触发单次扫码并维持实时视频预览

基于Vision Framework的扫码器需求实现:点击按钮触发单次扫码并维持实时视频预览

你的需求其实很常见——保持实时视频预览,仅在用户主动触发后才执行单次扫码,扫到结果后自动停止本次扫码,同时不中断视频流。你之前尝试通过添加/移除AVCaptureVideoDataOutput来控制扫码的开关,这种思路的问题在于AVCaptureSession的配置修改需要遵循严格的线程安全规则,而且频繁添加移除输出会导致状态不稳定,这也是你遇到Coordinator没被触发的核心原因。

更简洁稳定的方案是:让视频输出一直保持在会话中,仅在扫码逻辑的入口处根据绑定的状态控制是否执行条码检测。这样既不用修改captureSession的结构,又能精准控制扫码启停,同时视频预览全程不受影响。

完整实现代码

import SwiftUI
import AVFoundation
import Vision

struct ScannerView: UIViewControllerRepresentable {
    @Binding var scannedString: String
    @Binding var getScan: Bool // 控制单次扫码的开关
    
    let captureSession = AVCaptureSession()
    let videoOutput = AVCaptureVideoDataOutput()
    
    func makeUIViewController(context: Context) -> UIViewController {
        let viewController = UIViewController()
        
        // 1. 配置视频输入(相机权限需提前申请)
        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video),
              let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice),
              captureSession.canAddInput(videoInput) else {
            return viewController
        }
        captureSession.addInput(videoInput)
        
        // 2. 配置视频输出,绑定Coordinator作为委托
        videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]
        videoOutput.setSampleBufferDelegate(context.coordinator, queue: DispatchQueue(label: "videoQueue"))
        if captureSession.canAddOutput(videoOutput) {
            captureSession.addOutput(videoOutput)
        }
        
        // 3. 添加视频预览层
        let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
        previewLayer.frame = viewController.view.bounds
        previewLayer.videoGravity = .resizeAspectFill
        viewController.view.layer.addSublayer(previewLayer)
        
        // 4. 启动捕获会话(后台线程执行避免阻塞UI)
        DispatchQueue.global(qos: .background).async {
            captureSession.startRunning()
        }
        
        return viewController
    }
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        // 无需在此修改Session配置,扫码逻辑的开关在Coordinator中控制
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    // MARK: - Coordinator 实现
    class Coordinator: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
        let parent: ScannerView
        private let barcodeDetectionRequest = VNDetectBarcodesRequest(completionHandler: nil)
        
        init(_ parent: ScannerView) {
            self.parent = parent
            super.init()
            // 配置条码检测请求的结果回调
            barcodeDetectionRequest.completionHandler = { [weak self] request, error in
                self?.processBarcodeResults(request, error: error)
            }
        }
        
        // 视频帧回调:仅在getScan为true时执行扫码逻辑
        func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
            guard parent.getScan else { return }
            
            // 从SampleBuffer中提取像素数据,用于Vision检测
            guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
            
            // 执行Vision条码检测
            let requestHandler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, options: [:])
            do {
                try requestHandler.perform([barcodeDetectionRequest])
            } catch {
                print("条码检测失败:\(error.localizedDescription)")
            }
        }
        
        // 处理条码检测结果
        private func processBarcodeResults(_ request: VNRequest, error: Error?) {
            guard error == nil, let results = request.results as? [VNBarcodeObservation] else {
                return
            }
            
            // 取第一个有效条码结果
            guard let barcode = results.first, let barcodeContent = barcode.payloadStringValue else { return }
            
            // 主线程更新Binding值(UI操作必须在主线程)
            DispatchQueue.main.async {
                // 避免重复触发:仅当前处于扫码状态时更新结果
                if self.parent.getScan {
                    self.parent.scannedString = barcodeContent
                    // 关闭本次扫码,等待下一次按钮触发
                    self.parent.getScan = false
                }
            }
        }
    }
}

关键逻辑解释

  1. 扫码开关的控制
    captureOutput方法开头判断parent.getScan的状态,只有为true时才执行Vision条码检测,从根源上控制了扫码逻辑的启停,完全不需要修改AVCaptureSession的结构。

  2. Vision Framework的高效检测
    使用VNDetectBarcodesRequest专门处理条码识别,这是苹果官方优化的API,比手动解析图像数据更高效准确,支持几乎所有常见条码类型(QR码、Code128等)。

  3. 线程安全的状态更新
    因为视频帧回调在后台队列videoQueue执行,所以更新scannedStringgetScan这些绑定值时,必须切换到主线程(DispatchQueue.main.async),否则会导致UI异常。

  4. 避免重复触发
    在更新结果前再次判断getScan状态,防止检测结果返回延迟导致的重复赋值(比如用户在扫码过程中手动关闭了扫码开关)。

父视图使用示例

struct ContentView: View {
    @State private var scannedCode = ""
    @State private var isScanning = false
    
    var body: some View {
        ZStack {
            ScannerView(scannedString: $scannedCode, getScan: $isScanning)
                .ignoresSafeArea()
            
            VStack {
                Spacer()
                
                // 扫码触发按钮
                Button(action: {
                    scannedCode = "" // 清空上一次结果
                    isScanning = true // 启动单次扫码
                }) {
                    Text(isScanning ? "正在扫码..." : "点击扫码")
                        .padding()
                        .background(isScanning ? Color.gray : Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(12)
                }
                .padding(.bottom, 50)
                
                // 扫码结果展示
                if !scannedCode.isEmpty {
                    Text("扫码结果:\(scannedCode)")
                        .padding()
                        .background(Color.white.opacity(0.9))
                        .cornerRadius(8)
                        .padding(.bottom, 20)
                }
            }
        }
        .onAppear {
            // 可在此处提前申请相机权限
        }
    }
}

注意事项

  • 必须在Info.plist中添加相机权限描述:
    <key>NSCameraUsageDescription</key>
    <string>需要使用相机进行条码扫描</string>
    
  • 如果需要限制检测特定类型的条码,可以在初始化VNDetectBarcodesRequest后设置symbologies属性,比如:
    barcodeDetectionRequest.symbologies = [.qr, .code128]
    
  • 该方案全程保持视频预览,扫码逻辑仅在用户触发后短暂执行,性能和稳定性都远优于频繁修改AVCaptureSession的方案。

火山引擎 最新活动