import SwiftUI import AVFoundation import AudioToolbox import Combine // MARK: - 扫描器视图模型 class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate { @Published var detectedCodes: [DetectedCode] = [] @Published var showAlert = false @Published var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined @Published var isTorchOn = false var captureSession: AVCaptureSession! private var metadataOutput: AVCaptureMetadataOutput? private var videoDevice: AVCaptureDevice? private var isProcessingDetection = false // 添加处理状态标记 override init() { super.init() checkCameraPermission() } // MARK: - 相机权限管理 private func checkCameraPermission() { logInfo("🔍 检查相机权限状态", className: "ScannerViewModel") switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: logInfo("✅ 相机权限已授权,立即设置捕获会话", className: "ScannerViewModel") cameraAuthorizationStatus = .authorized setupCaptureSession() // setupCaptureSession 现在会自动启动扫描 case .notDetermined: logInfo("❓ 相机权限未确定,请求权限", className: "ScannerViewModel") requestCameraPermission() case .denied, .restricted: logWarning("❌ 相机权限被拒绝或受限", className: "ScannerViewModel") cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) @unknown default: logWarning("❓ 未知的相机权限状态", className: "ScannerViewModel") cameraAuthorizationStatus = .notDetermined } } private func requestCameraPermission() { logInfo("🔐 请求相机权限", className: "ScannerViewModel") AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in DispatchQueue.main.async { if granted { logInfo("✅ 相机权限请求成功,立即设置捕获会话", className: "ScannerViewModel") self?.cameraAuthorizationStatus = .authorized self?.setupCaptureSession() // setupCaptureSession 现在会自动启动扫描 } else { logWarning("❌ 相机权限请求被拒绝", className: "ScannerViewModel") self?.cameraAuthorizationStatus = .denied } } } } func openSettings() { logInfo("⚙️ 打开系统设置", className: "ScannerViewModel") if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(settingsUrl) { success in if success { logInfo("✅ 成功打开系统设置", className: "ScannerViewModel") } else { logWarning("⚠️ 打开系统设置失败", className: "ScannerViewModel") } } } } func refreshCameraPermission() { logInfo("🔍 重新检查相机权限状态", className: "ScannerViewModel") // 先停止当前扫描 if captureSession?.isRunning == true { stopScanning() } // 重新检查权限 checkCameraPermission() } // MARK: - 相机设置 private func setupCaptureSession() { logInfo("🔧 开始设置捕获会话", className: "ScannerViewModel") captureSession = AVCaptureSession() guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { logError("❌ 无法获取视频设备", className: "ScannerViewModel") showAlert = true return } // 保存视频设备引用,用于手电筒控制 videoDevice = videoCaptureDevice let videoInput: AVCaptureDeviceInput do { videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) } catch { logError("❌ 创建视频输入失败: \(error.localizedDescription)", className: "ScannerViewModel") showAlert = true return } if captureSession.canAddInput(videoInput) { captureSession.addInput(videoInput) logInfo("✅ 成功添加视频输入", className: "ScannerViewModel") } else { logError("❌ 无法添加视频输入", className: "ScannerViewModel") showAlert = true return } metadataOutput = AVCaptureMetadataOutput() if let metadataOutput = metadataOutput, captureSession.canAddOutput(metadataOutput) { captureSession.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .code128, .code39, .upce, .pdf417, .aztec] logInfo("✅ 成功添加元数据输出", className: "ScannerViewModel") } else { logError("❌ 无法添加元数据输出", className: "ScannerViewModel") showAlert = true return } logInfo("✅ 捕获会话设置完成,准备启动扫描", className: "ScannerViewModel") // 设置完成后立即启动扫描 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in self?.startScanning() } } // MARK: - 扫描控制 func startScanning() { logInfo("🔄 开始扫描", className: "ScannerViewModel") // 检查相机权限 guard cameraAuthorizationStatus == .authorized else { logWarning("❌ 相机权限未授权,无法启动扫描", className: "ScannerViewModel") return } // 检查捕获会话是否已设置 guard captureSession != nil else { logWarning("⚠️ 捕获会话未设置,重新设置", className: "ScannerViewModel") setupCaptureSession() return } // 检查会话是否已经在运行 if captureSession.isRunning { logInfo("ℹ️ 扫描会话已经在运行", className: "ScannerViewModel") return } // 检查会话配置是否完整 if captureSession.inputs.isEmpty || captureSession.outputs.isEmpty { logWarning("⚠️ 捕获会话配置不完整,重新设置", className: "ScannerViewModel") setupCaptureSession() return } logInfo("🚀 启动扫描会话", className: "ScannerViewModel") DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } self.captureSession?.startRunning() // 检查启动状态 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { if self.captureSession?.isRunning == true { logInfo("✅ 扫描会话启动成功", className: "ScannerViewModel") } else { logWarning("⚠️ 扫描会话启动失败,尝试重试", className: "ScannerViewModel") // 启动失败时重试一次 DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.retryStartScanning() } } } } } private func retryStartScanning() { logInfo("🔄 重试启动扫描", className: "ScannerViewModel") guard captureSession != nil else { logError("❌ 捕获会话为空,无法重试", className: "ScannerViewModel") return } DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.captureSession?.startRunning() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { if self?.captureSession?.isRunning == true { logInfo("✅ 重试启动扫描成功", className: "ScannerViewModel") } else { logError("❌ 重试启动扫描失败", className: "ScannerViewModel") } } } } func stopScanning() { logInfo("🔄 停止扫描", className: "ScannerViewModel") // 立即设置处理标记,防止新的检测被处理 isProcessingDetection = true // 检查会话是否在运行 if captureSession?.isRunning == true { // 立即停止扫描会话 captureSession?.stopRunning() logInfo("✅ 扫描会话已停止", className: "ScannerViewModel") } else { logInfo("ℹ️ 扫描会话已经停止", className: "ScannerViewModel") } } /// 暂停相机功能(用于预览暂停状态) func pauseCamera() { logInfo("⏸️ 暂停相机功能", className: "ScannerViewModel") // 设置处理标记 isProcessingDetection = true // 停止会话(但保留配置) if captureSession?.isRunning == true { captureSession?.stopRunning() logInfo("✅ 相机会话已暂停", className: "ScannerViewModel") } else { logInfo("ℹ️ 相机会话已经停止", className: "ScannerViewModel") } } /// 恢复相机功能(用于恢复扫描) func resumeCamera() { logInfo("▶️ 恢复相机功能", className: "ScannerViewModel") // 检查相机权限 guard cameraAuthorizationStatus == .authorized else { logWarning("❌ 相机权限未授权,无法恢复相机", className: "ScannerViewModel") return } // 检查会话配置 if captureSession == nil || captureSession.inputs.isEmpty || captureSession.outputs.isEmpty { logInfo("🔄 重新设置相机会话", className: "ScannerViewModel") setupCaptureSession() } // 重置处理标记 isProcessingDetection = false // 启动相机会话 if captureSession?.isRunning != true { logInfo("🚀 启动相机会话", className: "ScannerViewModel") DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.captureSession?.startRunning() DispatchQueue.main.async { if self?.captureSession?.isRunning == true { logInfo("✅ 相机会话启动成功", className: "ScannerViewModel") } else { logWarning("⚠️ 相机会话启动失败", className: "ScannerViewModel") } } } } logInfo("✅ 相机功能已恢复", className: "ScannerViewModel") } func resetDetection() { DispatchQueue.main.async { logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel") self.detectedCodes = [] self.isProcessingDetection = false // 重置处理标记 } } func isSessionRunning() -> Bool { return captureSession?.isRunning == true } func checkSessionStatus() { let isRunning = captureSession?.isRunning == true logInfo("📊 扫描会话状态检查: \(isRunning ? "运行中" : "已停止")", className: "ScannerViewModel") if !isRunning { logWarning("⚠️ 扫描会话未运行,尝试重新启动", className: "ScannerViewModel") startScanning() } } func restartScanning() { logInfo("🔄 重新开始扫描", className: "ScannerViewModel") // 先停止当前会话 if captureSession?.isRunning == true { logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel") captureSession?.stopRunning() } // 重置检测状态 resetDetection() // 延迟后重新启动会话 DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel") // 在后台线程启动会话 DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.captureSession?.startRunning() // 检查会话状态 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { if self?.captureSession?.isRunning == true { logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel") } else { logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel") } } } } } // MARK: - AVCaptureMetadataOutputObjectsDelegate func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { // 防止重复处理检测结果 guard !isProcessingDetection else { logInfo("⚠️ 正在处理检测结果,忽略新的检测", className: "ScannerViewModel") return } logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel") // 设置处理标记 isProcessingDetection = true // 震动反馈 AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) // 停止扫描 stopScanning() // 处理所有检测到的条码 var codes: [DetectedCode] = [] for metadataObject in metadataObjects { if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, let stringValue = readableObject.stringValue { let codeType = getBarcodeTypeString(from: readableObject.type) let bounds = readableObject.bounds let detectedCode = DetectedCode( type: codeType, content: stringValue, bounds: bounds, source: .camera ) codes.append(detectedCode) logInfo("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)", className: "ScannerViewModel") } } logInfo("准备更新 detectedCodes,数量: \(codes.count)", className: "ScannerViewModel") // 更新检测到的条码列表 DispatchQueue.main.async { logInfo("在主线程更新 detectedCodes", className: "ScannerViewModel") self.detectedCodes = codes } } // MARK: - 手电筒控制 /// 检查设备是否支持手电筒 var isTorchAvailable: Bool { guard let device = videoDevice else { return false } return device.hasTorch && device.isTorchAvailable } /// 切换手电筒状态 func toggleTorch() { guard let device = videoDevice else { logWarning("❌ 没有可用的视频设备", className: "ScannerViewModel") return } guard device.hasTorch && device.isTorchAvailable else { logWarning("❌ 设备不支持手电筒", className: "ScannerViewModel") return } do { try device.lockForConfiguration() if isTorchOn { // 关闭手电筒 device.torchMode = .off isTorchOn = false logInfo("🔦 手电筒已关闭", className: "ScannerViewModel") } else { // 打开手电筒 try device.setTorchModeOn(level: 1.0) isTorchOn = true logInfo("🔦 手电筒已打开", className: "ScannerViewModel") } device.unlockForConfiguration() } catch { logError("❌ 手电筒控制失败: \(error.localizedDescription)", className: "ScannerViewModel") device.unlockForConfiguration() } } /// 关闭手电筒 func turnOffTorch() { guard let device = videoDevice else { return } do { try device.lockForConfiguration() device.torchMode = .off isTorchOn = false device.unlockForConfiguration() logInfo("🔦 手电筒已关闭", className: "ScannerViewModel") } catch { logError("❌ 关闭手电筒失败: \(error.localizedDescription)", className: "ScannerViewModel") device.unlockForConfiguration() } } // MARK: - 条形码类型转换 /// 获取条形码类型的可读字符串 private func getBarcodeTypeString(from metadataType: AVMetadataObject.ObjectType) -> String { switch metadataType { case .ean8: return "EAN-8" case .ean13: return "EAN-13" case .upce: return "UPC-E" case .code39: return "Code 39" case .code93: return "Code 93" case .code128: return "Code 128" case .itf14: return "ITF-14" case .pdf417: return "PDF417" case .qr: return "QR Code" case .dataMatrix: return "Data Matrix" case .aztec: return "Aztec" default: // 处理可能包含前缀的类型字符串 let typeString = metadataType.rawValue if typeString.contains("org.gs1.") { // 移除 org.gs1. 前缀 let cleanType = typeString.replacingOccurrences(of: "org.gs1.", with: "") return cleanType.uppercased() } return typeString } } }