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 10 months ago
parent e526f6cbce
commit b91e461ad9

@ -3,6 +3,7 @@ import AVFoundation
import AudioToolbox
import Combine
import CoreData
import QRCode
// MARK: -
struct ScannerView: View {
@ -14,6 +15,11 @@ struct ScannerView: View {
@State private var navigateToDetail = false
@State private var selectedHistoryItem: HistoryItem?
//
@State private var showImagePicker = false
@State private var isDecodingImage = false
@State private var decodedImageCodes: [DetectedCode] = []
var body: some View {
ZStack {
//
@ -26,24 +32,28 @@ struct ScannerView: View {
ScanningOverlayView(
showPreviewPause: showPreviewPause,
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(
detectedCodes: scannerViewModel.detectedCodes,
detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes,
previewLayer: previewLayer,
onCodeSelected: handleCodeSelection
)
}
//
if showPreviewPause && scannerViewModel.detectedCodes.count == 1 {
TestAutoSelectButton(
detectedCode: scannerViewModel.detectedCodes[0],
onSelect: handleCodeSelection
)
if showPreviewPause && (scannerViewModel.detectedCodes.count + decodedImageCodes.count) == 1 {
let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first
if let code = singleCode {
TestAutoSelectButton(
detectedCode: code,
onSelect: handleCodeSelection
)
}
}
} else {
// UI
@ -61,6 +71,9 @@ struct ScannerView: View {
.navigationTitle("扫描器")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(false)
.sheet(isPresented: $showImagePicker) {
ImagePicker(onImageSelected: handleImageDecodeResult)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
// -
@ -246,6 +259,9 @@ struct ScannerView: View {
//
scannerViewModel.resetDetection()
//
resetImageDecodeState()
//
scannerViewModel.resumeCamera()
@ -256,7 +272,8 @@ struct ScannerView: View {
logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
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")
return
}
@ -265,6 +282,96 @@ struct ScannerView: View {
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

@ -1,10 +1,12 @@
import SwiftUI
import AudioToolbox
// MARK: -
struct ScanningOverlayView: View {
let showPreviewPause: Bool
@Binding var selectedStyle: ScanningLineStyle
let detectedCodesCount: Int
let onImageDecode: () -> Void
var body: some View {
VStack {
@ -26,7 +28,8 @@ struct ScanningOverlayView: View {
//
ScanningBottomButtonsView(
showPreviewPause: showPreviewPause,
selectedStyle: $selectedStyle
selectedStyle: $selectedStyle,
onImageDecode: onImageDecode
)
}
}
@ -68,6 +71,7 @@ struct ScanningInstructionView: View {
struct ScanningBottomButtonsView: View {
let showPreviewPause: Bool
@Binding var selectedStyle: ScanningLineStyle
let onImageDecode: () -> Void
var body: some View {
VStack(spacing: 15) {
@ -76,6 +80,34 @@ struct ScanningBottomButtonsView: View {
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)
@ -87,19 +119,104 @@ struct ScanningStyleSelectorView: View {
@Binding var selectedStyle: ScanningLineStyle
var body: some View {
HStack(spacing: 10) {
ForEach(ScanningLineStyle.allCases, id: \.self) { style in
Button(style.localizedName) {
selectedStyle = style
}
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(selectedStyle == style ? Color.green : Color.gray.opacity(0.6))
.cornerRadius(8)
VStack(spacing: 12) {
//
Text("扫描线样式")
.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)
}
//
@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