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.

main
v504 2 months ago
parent 8b2ea07f68
commit 1fdc94e103

@ -4,6 +4,7 @@ import AudioToolbox
import Combine import Combine
import CoreData import CoreData
import QRCode import QRCode
import Vision
// MARK: - // MARK: -
struct ScannerView: View { struct ScannerView: View {
@ -18,6 +19,8 @@ struct ScannerView: View {
@State private var showImagePicker = false @State private var showImagePicker = false
@State private var isDecodingImage = false @State private var isDecodingImage = false
@State private var decodedImageCodes: [DetectedCode] = [] @State private var decodedImageCodes: [DetectedCode] = []
@State private var showDecodeFailure = false
@State private var decodeFailureMessage = ""
var body: some View { var body: some View {
ZStack { ZStack {
@ -27,15 +30,18 @@ struct ScannerView: View {
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
.ignoresSafeArea() .ignoresSafeArea()
// //
ScanningOverlayView( ScanningOverlayView(
showPreviewPause: showPreviewPause, showPreviewPause: showPreviewPause &&
detectedCodesCount: scannerViewModel.detectedCodes.count, ((scannerViewModel.detectedCodes.count > 1) || (decodedImageCodes.count > 1)),
onImageDecode: { showImagePicker = true } 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( CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes, detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes,
previewLayer: previewLayer, 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 let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first
if let code = singleCode { if let code = singleCode {
TestAutoSelectButton( TestAutoSelectButton(
@ -53,6 +60,16 @@ struct ScannerView: View {
) )
} }
} }
//
if showDecodeFailure {
DecodeFailureOverlay(
message: decodeFailureMessage,
onDismiss: {
showDecodeFailure = false
}
)
}
} else { } else {
// UI // UI
CameraPermissionView( CameraPermissionView(
@ -287,42 +304,65 @@ struct ScannerView: View {
private func handleImageDecodeResult(_ image: UIImage) { private func handleImageDecodeResult(_ image: UIImage) {
isDecodingImage = true isDecodingImage = true
decodedImageCodes.removeAll() decodedImageCodes.removeAll()
showDecodeFailure = false
decodeFailureMessage = ""
logInfo("🔍 开始解码图片", className: "ScannerView") logInfo("🔍 开始解码图片", className: "ScannerView")
// 线 // 线
DispatchQueue.global(qos: .userInitiated).async { [self] in DispatchQueue.global(qos: .userInitiated).async { [self] in
var allResults: [DetectedCode] = []
// 使QRCode // 使Vision
let detected = QRCode.DetectQRCodes(in: image) if let cgImage = image.cgImage {
if detected.count > 0 { let barcodeResults = detectBarcodes(in: cgImage)
logInfo("✅ 检测到 \(detected.count) 个二维码", className: "ScannerView") if !barcodeResults.isEmpty {
logInfo("✅ 检测到 \(barcodeResults.count) 个条码", className: "ScannerView")
let results = detected.enumerated().map { index, qrCode in allResults.append(contentsOf: barcodeResults)
DetectedCode(
type: "QR Code",
content: qrCode.messageString ?? "未知内容",
bounds: qrCode.bounds
)
} }
}
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.isDecodingImage = false
self.pauseForPreview() logInfo("✅ 图片解码完成,去重后共 \(uniqueResults.count) 个结果", className: "ScannerView")
logInfo("✅ 图片解码完成,共 \(results.count) 个结果", className: "ScannerView")
// //
if results.count == 1 { if uniqueResults.count == 1 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { //
self.handleCodeSelection(results[0]) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.handleCodeSelection(uniqueResults[0])
} }
} else if uniqueResults.count > 1 {
//
self.pauseForPreview()
logInfo("📱 检测到多个条码,显示选择点", className: "ScannerView")
} }
} } else {
} else {
DispatchQueue.main.async {
self.isDecodingImage = false self.isDecodingImage = false
logWarning("❌ 图片中未检测到二维码", className: "ScannerView") self.decodeFailureMessage = "图片中未检测到二维码或条形码"
self.showDecodeFailure = true
logWarning("❌ 图片中未检测到二维码或条形码", className: "ScannerView")
} }
} }
} }
@ -331,6 +371,119 @@ struct ScannerView: View {
/// ///
private func resetImageDecodeState() { private func resetImageDecodeState() {
decodedImageCodes.removeAll() 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<String> = []
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 #if DEBUG
struct ScannerView_Previews: PreviewProvider { struct ScannerView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {

Loading…
Cancel
Save