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 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,25 +32,29 @@ 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 {
if showPreviewPause && (scannerViewModel.detectedCodes.count + decodedImageCodes.count) == 1 {
let singleCode = scannerViewModel.detectedCodes.first ?? decodedImageCodes.first
if let code = singleCode {
TestAutoSelectButton(
detectedCode: scannerViewModel.detectedCodes[0],
detectedCode: code,
onSelect: handleCodeSelection
)
}
}
} else {
// UI
CameraPermissionView(
@ -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) {
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(style.localizedName) {
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)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(selectedStyle == style ? Color.green : Color.gray.opacity(0.6))
.cornerRadius(8)
.font(.caption)
}
.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