基于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 } } } } }
关键逻辑解释
扫码开关的控制
在captureOutput方法开头判断parent.getScan的状态,只有为true时才执行Vision条码检测,从根源上控制了扫码逻辑的启停,完全不需要修改AVCaptureSession的结构。Vision Framework的高效检测
使用VNDetectBarcodesRequest专门处理条码识别,这是苹果官方优化的API,比手动解析图像数据更高效准确,支持几乎所有常见条码类型(QR码、Code128等)。线程安全的状态更新
因为视频帧回调在后台队列videoQueue执行,所以更新scannedString和getScan这些绑定值时,必须切换到主线程(DispatchQueue.main.async),否则会导致UI异常。避免重复触发
在更新结果前再次判断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的方案。




