From 1fdc94e103d2011358fd81fe65f8c873e8503487 Mon Sep 17 00:00:00 2001 From: v504 Date: Thu, 21 Aug 2025 14:11:26 +0800 Subject: [PATCH] Enhance ScannerView with Vision framework integration for barcode detection; implement decode failure handling with user feedback overlay, improving error management and user experience during image decoding. --- MyQrCode/ScannerView/ScannerView.swift | 297 ++++++++++++++++++++++--- 1 file changed, 263 insertions(+), 34 deletions(-) diff --git a/MyQrCode/ScannerView/ScannerView.swift b/MyQrCode/ScannerView/ScannerView.swift index 7fc7bf7..2997980 100644 --- a/MyQrCode/ScannerView/ScannerView.swift +++ b/MyQrCode/ScannerView/ScannerView.swift @@ -4,6 +4,7 @@ import AudioToolbox import Combine import CoreData import QRCode +import Vision // MARK: - 主扫描视图 struct ScannerView: View { @@ -18,6 +19,8 @@ struct ScannerView: View { @State private var showImagePicker = false @State private var isDecodingImage = false @State private var decodedImageCodes: [DetectedCode] = [] + @State private var showDecodeFailure = false + @State private var decodeFailureMessage = "" var body: some View { ZStack { @@ -27,15 +30,18 @@ struct ScannerView: View { CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) .ignoresSafeArea() - // 扫描界面覆盖层 - ScanningOverlayView( - showPreviewPause: showPreviewPause, - detectedCodesCount: scannerViewModel.detectedCodes.count, - onImageDecode: { showImagePicker = true } - ) + // 扫描界面覆盖层 + ScanningOverlayView( + showPreviewPause: showPreviewPause && + ((scannerViewModel.detectedCodes.count > 1) || (decodedImageCodes.count > 1)), + detectedCodesCount: scannerViewModel.detectedCodes.count + decodedImageCodes.count, + onImageDecode: { showImagePicker = true } + ) - // 条码位置标记覆盖层 - if showPreviewPause && (!scannerViewModel.detectedCodes.isEmpty || !decodedImageCodes.isEmpty) { + // 条码位置标记覆盖层 - 只在需要用户选择时显示 + if showPreviewPause && + ((!scannerViewModel.detectedCodes.isEmpty && scannerViewModel.detectedCodes.count > 1) || + (!decodedImageCodes.isEmpty && decodedImageCodes.count > 1)) { CodePositionOverlay( detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes, previewLayer: previewLayer, @@ -43,8 +49,9 @@ struct ScannerView: View { ) } - // 测试按钮(调试用) - if showPreviewPause && (scannerViewModel.detectedCodes.count + decodedImageCodes.count) == 1 { + // 测试按钮(调试用)- 只在需要用户选择时显示 + if showPreviewPause && + ((scannerViewModel.detectedCodes.count > 1) || (decodedImageCodes.count > 1)) { let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first if let code = singleCode { TestAutoSelectButton( @@ -53,6 +60,16 @@ struct ScannerView: View { ) } } + + // 解码失败提示 + if showDecodeFailure { + DecodeFailureOverlay( + message: decodeFailureMessage, + onDismiss: { + showDecodeFailure = false + } + ) + } } else { // 权限未授权时的UI CameraPermissionView( @@ -287,42 +304,65 @@ struct ScannerView: View { private func handleImageDecodeResult(_ image: UIImage) { isDecodingImage = true decodedImageCodes.removeAll() + showDecodeFailure = false + decodeFailureMessage = "" logInfo("🔍 开始解码图片", className: "ScannerView") // 在后台线程进行解码 DispatchQueue.global(qos: .userInitiated).async { [self] in + var allResults: [DetectedCode] = [] - // 使用QRCode库检测二维码 - let detected = QRCode.DetectQRCodes(in: image) - if detected.count > 0 { - logInfo("✅ 检测到 \(detected.count) 个二维码", className: "ScannerView") - - let results = detected.enumerated().map { index, qrCode in - DetectedCode( - type: "QR Code", - content: qrCode.messageString ?? "未知内容", - bounds: qrCode.bounds - ) + // 使用Vision框架检测所有条码(包括二维码和条形码) + if let cgImage = image.cgImage { + let barcodeResults = detectBarcodes(in: cgImage) + if !barcodeResults.isEmpty { + logInfo("✅ 检测到 \(barcodeResults.count) 个条码", className: "ScannerView") + allResults.append(contentsOf: barcodeResults) } - - DispatchQueue.main.async { - self.decodedImageCodes = results + } + + // 如果没有检测到条码,尝试使用QRCode库作为备用方案 + if allResults.isEmpty { + let detectedQR = QRCode.DetectQRCodes(in: image) + if detectedQR.count > 0 { + logInfo("✅ 使用QRCode库检测到 \(detectedQR.count) 个二维码", className: "ScannerView") + + let qrResults = detectedQR.enumerated().map { index, qrCode in + DetectedCode( + type: "QR Code", + content: qrCode.messageString ?? "未知内容", + bounds: qrCode.bounds + ) + } + allResults.append(contentsOf: qrResults) + } + } + + DispatchQueue.main.async { + if !allResults.isEmpty { + // 去重:移除相同内容的重复条码 + let uniqueResults = self.removeDuplicateCodes(allResults) + self.decodedImageCodes = uniqueResults self.isDecodingImage = false - self.pauseForPreview() - logInfo("✅ 图片解码完成,共 \(results.count) 个结果", className: "ScannerView") + logInfo("✅ 图片解码完成,去重后共 \(uniqueResults.count) 个结果", className: "ScannerView") - // 如果只有一个二维码,自动选择并跳转 - if results.count == 1 { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.handleCodeSelection(results[0]) + // 图片解码结果直接跳转,不显示选择点 + if uniqueResults.count == 1 { + // 直接处理单个条码,跳转到结果页 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.handleCodeSelection(uniqueResults[0]) } + } else if uniqueResults.count > 1 { + // 多个条码时,显示选择点让用户选择 + self.pauseForPreview() + logInfo("📱 检测到多个条码,显示选择点", className: "ScannerView") } - } - } else { - DispatchQueue.main.async { + } else { self.isDecodingImage = false - logWarning("❌ 图片中未检测到二维码", className: "ScannerView") + self.decodeFailureMessage = "图片中未检测到二维码或条形码" + self.showDecodeFailure = true + logWarning("❌ 图片中未检测到二维码或条形码", className: "ScannerView") } } } @@ -331,6 +371,119 @@ struct ScannerView: View { /// 重置图片解码状态 private func resetImageDecodeState() { decodedImageCodes.removeAll() + showDecodeFailure = false + decodeFailureMessage = "" + } + + /// 使用Vision框架检测条形码 + private func detectBarcodes(in cgImage: CGImage) -> [DetectedCode] { + let request = VNDetectBarcodesRequest { request, error in + if let error = error { + logWarning("条形码检测错误: \(error.localizedDescription)", className: "ScannerView") + return + } + } + + // 设置条形码类型 + request.symbologies = [ + .ean8, + .ean13, + .upce, + .code39, + .code39Checksum, + .code39FullASCII, + .code39FullASCIIChecksum, + .code93, + .code93i, + .code128, + .itf14, + .pdf417, + .qr, + .dataMatrix, + .aztec + ] + + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + + do { + try handler.perform([request]) + + let results = request.results ?? [] + guard !results.isEmpty else { + return [] + } + + return results.enumerated().map { index, observation in + let barcodeType = getBarcodeTypeString(from: observation.symbology) + let content = observation.payloadStringValue ?? "未知内容" + + logInfo("检测到条形码 #\(index + 1): 类型=\(barcodeType), 内容=\(content)", className: "ScannerView") + + return DetectedCode( + type: barcodeType, + content: content, + bounds: observation.boundingBox + ) + } + } catch { + logWarning("条形码检测请求失败: \(error.localizedDescription)", className: "ScannerView") + return [] + } + } + + /// 获取条形码类型的可读字符串 + private func getBarcodeTypeString(from symbology: VNBarcodeSymbology) -> String { + switch symbology { + case .ean8: + return "EAN-8" + case .ean13: + return "EAN-13" + case .upce: + return "UPC-E" + case .code39: + return "Code 39" + case .code39Checksum: + return "Code 39 (Checksum)" + case .code39FullASCII: + return "Code 39 (Full ASCII)" + case .code39FullASCIIChecksum: + return "Code 39 (Full ASCII + Checksum)" + case .code93: + return "Code 93" + case .code93i: + return "Code 93i" + 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: + return "Unknown Barcode" + } + } + + /// 移除重复的条码(基于内容去重) + private func removeDuplicateCodes(_ codes: [DetectedCode]) -> [DetectedCode] { + var uniqueCodes: [DetectedCode] = [] + var seenContents: Set = [] + + for code in codes { + if !seenContents.contains(code.content) { + seenContents.insert(code.content) + uniqueCodes.append(code) + } else { + logInfo("🔄 发现重复条码,内容: \(code.content),已跳过", className: "ScannerView") + } + } + + return uniqueCodes } } @@ -372,6 +525,82 @@ struct ImagePicker: UIViewControllerRepresentable { } } +// MARK: - 解码失败提示覆盖层 +struct DecodeFailureOverlay: View { + let message: String + let onDismiss: () -> Void + + var body: some View { + ZStack { + // 半透明背景 + Color.black.opacity(0.7) + .ignoresSafeArea() + .onTapGesture { + onDismiss() + } + + // 失败提示卡片 + VStack(spacing: 20) { + // 失败图标 + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 50)) + .foregroundColor(.orange) + + // 失败标题 + Text("解码失败") + .font(.title2) + .fontWeight(.bold) + .foregroundColor(.white) + + // 失败消息 + Text(message) + .font(.body) + .foregroundColor(.white.opacity(0.8)) + .multilineTextAlignment(.center) + .padding(.horizontal, 20) + + // 重试按钮 + Button(action: { + onDismiss() + }) { + HStack(spacing: 8) { + Image(systemName: "arrow.clockwise") + .font(.system(size: 16, weight: .semibold)) + + Text("重新选择图片") + .font(.headline) + .fontWeight(.medium) + } + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.blue.opacity(0.8)) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.blue, lineWidth: 1) + ) + ) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(30) + .background( + RoundedRectangle(cornerRadius: 20) + .fill(Color(.systemGray6).opacity(0.9)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + ) + .shadow(color: .black.opacity(0.3), radius: 20, x: 0, y: 10) + } + .zIndex(2000) // 确保在最上层 + .transition(.opacity.combined(with: .scale)) + } +} + #if DEBUG struct ScannerView_Previews: PreviewProvider { static var previews: some View {