From e526f6cbceeb33cc2fba1c17e6054ec32a5eafc3 Mon Sep 17 00:00:00 2001 From: v504 Date: Thu, 21 Aug 2025 11:44:34 +0800 Subject: [PATCH] Enhance CameraPreviewView and ScannerView with improved session management and UI updates; add pause and resume functionality for camera, prevent duplicate detection processing, and ensure proper handling of scanning state transitions for better user experience. --- MyQrCode/ScannerView/CameraPreviewView.swift | 14 +- .../ScannerView/CodePositionOverlay.swift | 11 +- MyQrCode/ScannerView/ScannerView.swift | 25 ++-- MyQrCode/ScannerView/ScannerViewModel.swift | 133 ++++++++++++------ 4 files changed, 122 insertions(+), 61 deletions(-) diff --git a/MyQrCode/ScannerView/CameraPreviewView.swift b/MyQrCode/ScannerView/CameraPreviewView.swift index 8736ca6..855ba5f 100644 --- a/MyQrCode/ScannerView/CameraPreviewView.swift +++ b/MyQrCode/ScannerView/CameraPreviewView.swift @@ -8,11 +8,13 @@ struct CameraPreviewView: UIViewRepresentable { func makeUIView(context: Context) -> UIView { let view = UIView() + view.backgroundColor = .black + let previewLayer = AVCaptureVideoPreviewLayer(session: session) - previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) + // 设置预览层为绑定变量 DispatchQueue.main.async { self.previewLayer = previewLayer } @@ -21,12 +23,12 @@ struct CameraPreviewView: UIViewRepresentable { } func updateUIView(_ uiView: UIView, context: Context) { - if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer { + guard let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer else { return } + + // 确保预览层尺寸正确 + DispatchQueue.main.async { previewLayer.frame = uiView.bounds - - DispatchQueue.main.async { - self.previewLayer = previewLayer - } + self.previewLayer = previewLayer } } } \ No newline at end of file diff --git a/MyQrCode/ScannerView/CodePositionOverlay.swift b/MyQrCode/ScannerView/CodePositionOverlay.swift index d4008fb..460f10c 100644 --- a/MyQrCode/ScannerView/CodePositionOverlay.swift +++ b/MyQrCode/ScannerView/CodePositionOverlay.swift @@ -119,12 +119,16 @@ struct CodePositionMarker: View { return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } - guard previewLayer.session?.isRunning == true else { - logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker") + // 即使会话停止,我们仍然可以使用预览层进行坐标转换 + // 因为预览层的配置仍然有效 + let metadataObject = code.bounds + + // 检查边界值是否有效 + guard metadataObject.width > 0 && metadataObject.height > 0 else { + logWarning("Invalid metadata bounds: \(metadataObject), using screen center", className: "CodePositionMarker") return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } - let metadataObject = code.bounds let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint( x: metadataObject.midX, y: metadataObject.midY @@ -135,6 +139,7 @@ struct CodePositionMarker: View { return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } + // 确保选择点在屏幕范围内 let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x)) let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y)) diff --git a/MyQrCode/ScannerView/ScannerView.swift b/MyQrCode/ScannerView/ScannerView.swift index f61d838..7f99c0e 100644 --- a/MyQrCode/ScannerView/ScannerView.swift +++ b/MyQrCode/ScannerView/ScannerView.swift @@ -183,12 +183,19 @@ struct ScannerView: View { logInfo(" 选择的条码内容: \(selectedCode.content)", className: "ScannerView") logInfo(" 选择的条码位置: \(selectedCode.bounds)", className: "ScannerView") + // 停止扫描功能,避免在详情页面继续扫描 + scannerViewModel.stopScanning() + logInfo("🛑 已停止扫描功能", className: "ScannerView") + // 创建 HistoryItem 并保存到 Core Data let historyItem = createHistoryItem(from: selectedCode) - // 设置选中的历史记录项并导航到详情页 - selectedHistoryItem = historyItem - navigateToDetail = true + // 延迟一小段时间确保扫描完全停止后再跳转 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + // 设置选中的历史记录项并导航到详情页 + self.selectedHistoryItem = historyItem + self.navigateToDetail = true + } // 发送通知(保持向后兼容) let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)" @@ -226,6 +233,8 @@ struct ScannerView: View { private func pauseForPreview() { showPreviewPause = true + // 暂停相机功能,防止在预览暂停时继续拍照 + scannerViewModel.pauseCamera() } private func resetToScanning() { @@ -234,15 +243,11 @@ struct ScannerView: View { // 重置UI状态 showPreviewPause = false - // 重置扫描状态并重新开始 + // 重置扫描状态 scannerViewModel.resetDetection() - scannerViewModel.restartScanning() - // 延迟检查会话状态 - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - logInfo("🔍 检查扫描会话状态", className: "ScannerView") - self.scannerViewModel.checkSessionStatus() - } + // 恢复相机功能并重新开始扫描 + scannerViewModel.resumeCamera() logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView") } diff --git a/MyQrCode/ScannerView/ScannerViewModel.swift b/MyQrCode/ScannerView/ScannerViewModel.swift index 166687c..a04d9b5 100644 --- a/MyQrCode/ScannerView/ScannerViewModel.swift +++ b/MyQrCode/ScannerView/ScannerViewModel.swift @@ -14,6 +14,7 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec var captureSession: AVCaptureSession! private var metadataOutput: AVCaptureMetadataOutput? private var videoDevice: AVCaptureDevice? + private var isProcessingDetection = false // 添加处理状态标记 override init() { super.init() @@ -155,24 +156,80 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec 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?.stopRunning() + self?.captureSession?.startRunning() DispatchQueue.main.async { - logInfo("✅ 扫描会话已停止", className: "ScannerViewModel") + if self?.captureSession?.isRunning == true { + logInfo("✅ 相机会话启动成功", className: "ScannerViewModel") + } else { + logWarning("⚠️ 相机会话启动失败", className: "ScannerViewModel") + } } } - } else { - logInfo("ℹ️ 扫描会话已经停止", className: "ScannerViewModel") } + + logInfo("✅ 相机功能已恢复", className: "ScannerViewModel") } + + func resetDetection() { DispatchQueue.main.async { logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel") self.detectedCodes = [] + self.isProcessingDetection = false // 重置处理标记 } } @@ -193,46 +250,29 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec func restartScanning() { logInfo("🔄 重新开始扫描", className: "ScannerViewModel") - // 确保在主线程执行UI相关操作 - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // 先停止当前会话 - if self.captureSession?.isRunning == true { - logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel") - self.captureSession?.stopRunning() - } - - // 重置检测状态 - self.detectedCodes = [] + // 先停止当前会话 + if captureSession?.isRunning == true { + logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel") + captureSession?.stopRunning() + } + + // 重置检测状态 + resetDetection() + + // 延迟后重新启动会话 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel") - // 延迟后重新启动会话 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - - logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel") + // 在后台线程启动会话 + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + self?.captureSession?.startRunning() - // 在后台线程启动会话 - DispatchQueue.global(qos: .userInitiated).async { - self.captureSession?.startRunning() - - // 检查会话状态 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - if self.captureSession?.isRunning == true { - logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel") - } else { - logWarning("⚠️ 扫描会话启动失败,尝试重新启动", className: "ScannerViewModel") - // 如果启动失败,再次尝试 - DispatchQueue.global(qos: .userInitiated).async { - self.captureSession?.startRunning() - DispatchQueue.main.async { - if self.captureSession?.isRunning == true { - logInfo("✅ 扫描会话第二次尝试启动成功", className: "ScannerViewModel") - } else { - logError("❌ 扫描会话启动失败", className: "ScannerViewModel") - } - } - } - } + // 检查会话状态 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + if self?.captureSession?.isRunning == true { + logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel") + } else { + logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel") } } } @@ -245,8 +285,17 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec 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))