Add image decoding functionality to ScannerView; implement image picker for QR code detection from photos, enhance overlay with image decode button, and update state management for decoded images, improving user experience.

main
v504 2 months ago
parent e526f6cbce
commit b91e461ad9

@ -3,6 +3,7 @@ import AVFoundation
import AudioToolbox import AudioToolbox
import Combine import Combine
import CoreData import CoreData
import QRCode
// MARK: - // MARK: -
struct ScannerView: View { struct ScannerView: View {
@ -14,6 +15,11 @@ struct ScannerView: View {
@State private var navigateToDetail = false @State private var navigateToDetail = false
@State private var selectedHistoryItem: HistoryItem? @State private var selectedHistoryItem: HistoryItem?
//
@State private var showImagePicker = false
@State private var isDecodingImage = false
@State private var decodedImageCodes: [DetectedCode] = []
var body: some View { var body: some View {
ZStack { ZStack {
// //
@ -26,24 +32,28 @@ struct ScannerView: View {
ScanningOverlayView( ScanningOverlayView(
showPreviewPause: showPreviewPause, showPreviewPause: showPreviewPause,
selectedStyle: $selectedScanningStyle, selectedStyle: $selectedScanningStyle,
detectedCodesCount: scannerViewModel.detectedCodes.count detectedCodesCount: scannerViewModel.detectedCodes.count,
onImageDecode: { showImagePicker = true }
) )
// //
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { if showPreviewPause && (!scannerViewModel.detectedCodes.isEmpty || !decodedImageCodes.isEmpty) {
CodePositionOverlay( CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes, detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes,
previewLayer: previewLayer, previewLayer: previewLayer,
onCodeSelected: handleCodeSelection onCodeSelected: handleCodeSelection
) )
} }
// //
if showPreviewPause && scannerViewModel.detectedCodes.count == 1 { if showPreviewPause && (scannerViewModel.detectedCodes.count + decodedImageCodes.count) == 1 {
TestAutoSelectButton( let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first
detectedCode: scannerViewModel.detectedCodes[0], if let code = singleCode {
onSelect: handleCodeSelection TestAutoSelectButton(
) detectedCode: code,
onSelect: handleCodeSelection
)
}
} }
} else { } else {
// UI // UI
@ -61,6 +71,9 @@ struct ScannerView: View {
.navigationTitle("扫描器") .navigationTitle("扫描器")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(false) .navigationBarBackButtonHidden(false)
.sheet(isPresented: $showImagePicker) {
ImagePicker(onImageSelected: handleImageDecodeResult)
}
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
// - // -
@ -246,6 +259,9 @@ struct ScannerView: View {
// //
scannerViewModel.resetDetection() scannerViewModel.resetDetection()
//
resetImageDecodeState()
// //
scannerViewModel.resumeCamera() scannerViewModel.resumeCamera()
@ -256,7 +272,8 @@ struct ScannerView: View {
logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView") logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
guard self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 else { let totalCodes = self.scannerViewModel.detectedCodes.count + self.decodedImageCodes.count
guard self.showPreviewPause && totalCodes == 1 else {
logInfo("条件不满足,取消自动选择", className: "ScannerView") logInfo("条件不满足,取消自动选择", className: "ScannerView")
return return
} }
@ -265,6 +282,96 @@ struct ScannerView: View {
self.handleCodeSelection(code) self.handleCodeSelection(code)
} }
} }
// MARK: -
///
private func handleImageDecodeResult(_ image: UIImage) {
isDecodingImage = true
decodedImageCodes.removeAll()
logInfo("🔍 开始解码图片", className: "ScannerView")
// 线
DispatchQueue.global(qos: .userInitiated).async { [self] in
// 使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
)
}
DispatchQueue.main.async {
self.decodedImageCodes = results
self.isDecodingImage = false
self.pauseForPreview()
logInfo("✅ 图片解码完成,共 \(results.count) 个结果", className: "ScannerView")
//
if results.count == 1 {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.handleCodeSelection(results[0])
}
}
}
} else {
DispatchQueue.main.async {
self.isDecodingImage = false
logWarning("❌ 图片中未检测到二维码", className: "ScannerView")
}
}
}
}
///
private func resetImageDecodeState() {
decodedImageCodes.removeAll()
}
}
// MARK: - iOS 15
struct ImagePicker: UIViewControllerRepresentable {
let onImageSelected: (UIImage) -> Void
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
picker.allowsEditing = false
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
parent.onImageSelected(image)
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
}
} }
#if DEBUG #if DEBUG

@ -1,10 +1,12 @@
import SwiftUI import SwiftUI
import AudioToolbox
// MARK: - // MARK: -
struct ScanningOverlayView: View { struct ScanningOverlayView: View {
let showPreviewPause: Bool let showPreviewPause: Bool
@Binding var selectedStyle: ScanningLineStyle @Binding var selectedStyle: ScanningLineStyle
let detectedCodesCount: Int let detectedCodesCount: Int
let onImageDecode: () -> Void
var body: some View { var body: some View {
VStack { VStack {
@ -26,7 +28,8 @@ struct ScanningOverlayView: View {
// //
ScanningBottomButtonsView( ScanningBottomButtonsView(
showPreviewPause: showPreviewPause, showPreviewPause: showPreviewPause,
selectedStyle: $selectedStyle selectedStyle: $selectedStyle,
onImageDecode: onImageDecode
) )
} }
} }
@ -68,6 +71,7 @@ struct ScanningInstructionView: View {
struct ScanningBottomButtonsView: View { struct ScanningBottomButtonsView: View {
let showPreviewPause: Bool let showPreviewPause: Bool
@Binding var selectedStyle: ScanningLineStyle @Binding var selectedStyle: ScanningLineStyle
let onImageDecode: () -> Void
var body: some View { var body: some View {
VStack(spacing: 15) { VStack(spacing: 15) {
@ -76,6 +80,34 @@ struct ScanningBottomButtonsView: View {
ScanningStyleSelectorView(selectedStyle: $selectedStyle) ScanningStyleSelectorView(selectedStyle: $selectedStyle)
} }
//
if !showPreviewPause {
Button(action: {
onImageDecode()
}) {
HStack(spacing: 8) {
Image(systemName: "photo.on.rectangle.angled")
.font(.system(size: 16, weight: .semibold))
Text("图片解码")
.font(.subheadline)
.fontWeight(.medium)
}
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.8))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.blue, lineWidth: 1)
)
)
}
.buttonStyle(PlainButtonStyle())
}
// 使 // 使
} }
.padding(.bottom, 50) .padding(.bottom, 50)
@ -87,19 +119,104 @@ struct ScanningStyleSelectorView: View {
@Binding var selectedStyle: ScanningLineStyle @Binding var selectedStyle: ScanningLineStyle
var body: some View { var body: some View {
HStack(spacing: 10) { VStack(spacing: 12) {
ForEach(ScanningLineStyle.allCases, id: \.self) { style in //
Button(style.localizedName) { Text("扫描线样式")
selectedStyle = style
}
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(selectedStyle == style ? Color.green : Color.gray.opacity(0.6))
.cornerRadius(8)
.font(.caption) .font(.caption)
.foregroundColor(.white.opacity(0.8))
.padding(.bottom, 4)
//
HStack(spacing: 8) {
ForEach(ScanningLineStyle.allCases, id: \.self) { style in
Button(action: {
withAnimation(.easeInOut(duration: 0.2)) {
selectedStyle = style
}
//
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
}) {
VStack(spacing: 4) {
//
stylePreview(style)
.frame(width: 24, height: 24)
//
Text(style.localizedName)
.font(.caption2)
.foregroundColor(.white)
}
.frame(width: 60, height: 50)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(selectedStyle == style ?
Color.green.opacity(0.8) :
Color.black.opacity(0.6))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(selectedStyle == style ?
Color.green :
Color.white.opacity(0.3),
lineWidth: selectedStyle == style ? 2 : 1)
)
)
}
.buttonStyle(PlainButtonStyle())
}
} }
} }
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.black.opacity(0.7))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
)
.padding(.bottom, 10) .padding(.bottom, 10)
} }
//
@ViewBuilder
private func stylePreview(_ style: ScanningLineStyle) -> some View {
switch style {
case .modern:
Rectangle()
.fill(
LinearGradient(
colors: [.blue, .cyan, .blue],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: 20, height: 2)
.shadow(color: .blue, radius: 2, x: 0, y: 0)
case .classic:
Rectangle()
.fill(Color.green)
.frame(width: 16, height: 2)
case .neon:
Rectangle()
.fill(Color.purple)
.frame(width: 18, height: 3)
.shadow(color: .purple, radius: 3, x: 0, y: 0)
case .minimal:
Rectangle()
.fill(Color.white)
.frame(width: 14, height: 1)
case .retro:
Rectangle()
.fill(Color.orange)
.frame(width: 20, height: 2)
.overlay(
Rectangle()
.stroke(Color.yellow, lineWidth: 0.5)
.frame(width: 18, height: 1.5)
)
}
}
} }
Loading…
Cancel
Save