You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

383 lines
15 KiB

import SwiftUI
import AVFoundation
import AudioToolbox
import Combine
import CoreData
import QRCode
// MARK: -
struct ScannerView: View {
@StateObject private var scannerViewModel = ScannerViewModel()
@State private var showPreviewPause = false
@State private var screenOrientation = UIDevice.current.orientation
@State private var previewLayer: AVCaptureVideoPreviewLayer?
@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 {
//
if scannerViewModel.cameraAuthorizationStatus == .authorized {
//
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
.ignoresSafeArea()
//
ScanningOverlayView(
showPreviewPause: showPreviewPause,
detectedCodesCount: scannerViewModel.detectedCodes.count,
onImageDecode: { showImagePicker = true }
)
//
if showPreviewPause && (!scannerViewModel.detectedCodes.isEmpty || !decodedImageCodes.isEmpty) {
CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes + decodedImageCodes,
previewLayer: previewLayer,
onCodeSelected: 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
CameraPermissionView(
authorizationStatus: scannerViewModel.cameraAuthorizationStatus,
onRequestPermission: {
scannerViewModel.refreshCameraPermission()
},
onOpenSettings: {
scannerViewModel.openSettings()
}
)
}
}
.navigationTitle("扫描器")
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(false)
.sheet(isPresented: $showImagePicker) {
ImagePicker(onImageSelected: handleImageDecodeResult)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
// -
if scannerViewModel.cameraAuthorizationStatus == .authorized && scannerViewModel.isTorchAvailable {
Button(action: {
logInfo("🔦 用户点击手电筒按钮", className: "ScannerView")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
scannerViewModel.toggleTorch()
}) {
Image(systemName: scannerViewModel.isTorchOn ? "bolt.fill" : "bolt")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(scannerViewModel.isTorchOn ? .yellow : .blue)
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
// -
if showPreviewPause {
Button(action: {
logInfo("🔄 用户点击工具栏重新扫描按钮", className: "ScannerView")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
resetToScanning()
}) {
HStack(spacing: 6) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 16, weight: .semibold))
Text("rescan_button".localized)
.font(.system(size: 14, weight: .medium))
}
.foregroundColor(.blue)
}
}
}
}
.onAppear {
//
if scannerViewModel.cameraAuthorizationStatus == .authorized {
scannerViewModel.startScanning()
}
}
.onDisappear {
scannerViewModel.stopScanning()
// 退
if scannerViewModel.isTorchOn {
scannerViewModel.turnOffTorch()
}
}
.alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) {
Button("OK") { }
} message: {
Text("scan_error_message".localized)
}
.onReceive(scannerViewModel.$detectedCodes) { codes in
handleDetectedCodes(codes)
}
.onReceive(scannerViewModel.$cameraAuthorizationStatus) { status in
if status == .authorized {
logInfo("🎯 相机权限已授权,启动扫描", className: "ScannerView")
scannerViewModel.startScanning()
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
handleOrientationChange()
}
.background(
NavigationLink(
destination: Group {
if let historyItem = selectedHistoryItem {
QRCodeDetailView(historyItem: historyItem)
.onDisappear {
//
logInfo("🔄 从详情页返回,重新开始扫描", className: "ScannerView")
resetToScanning()
}
}
},
isActive: $navigateToDetail
) {
EmptyView()
}
)
}
// MARK: -
private func handleDetectedCodes(_ codes: [DetectedCode]) {
guard !codes.isEmpty else { return }
logInfo("检测到条码数量: \(codes.count)", className: "ScannerView")
if codes.count == 1 {
logInfo("单个条码,准备自动选择", className: "ScannerView")
pauseForPreview()
autoSelectSingleCode(code: codes[0])
} else {
logInfo("多个条码,等待用户选择", className: "ScannerView")
pauseForPreview()
}
}
private func handleOrientationChange() {
screenOrientation = UIDevice.current.orientation
logInfo("Screen orientation changed to: \(screenOrientation.rawValue)", className: "ScannerView")
}
private func handleCodeSelection(_ selectedCode: DetectedCode) {
logInfo("🎯 ScannerView 收到条码选择回调", className: "ScannerView")
logInfo(" 选择的条码ID: \(selectedCode.id)", className: "ScannerView")
logInfo(" 选择的条码类型: \(selectedCode.type)", className: "ScannerView")
logInfo(" 选择的条码内容: \(selectedCode.content)", className: "ScannerView")
logInfo(" 选择的条码位置: \(selectedCode.bounds)", className: "ScannerView")
//
scannerViewModel.stopScanning()
logInfo("🛑 已停止扫描功能", className: "ScannerView")
// HistoryItem Core Data
let historyItem = createHistoryItem(from: selectedCode)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
//
self.selectedHistoryItem = historyItem
self.navigateToDetail = true
}
//
let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)"
logInfo(" 格式化结果: \(formattedResult)", className: "ScannerView")
NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult)
}
private func createHistoryItem(from detectedCode: DetectedCode) -> HistoryItem {
let context = CoreDataManager.shared.container.viewContext
let historyItem = HistoryItem(context: context)
historyItem.id = UUID()
historyItem.content = detectedCode.content
historyItem.dataType = DataType.qrcode.rawValue
historyItem.dataSource = DataSource.scanned.rawValue
historyItem.createdAt = Date()
historyItem.isFavorite = false
//
if detectedCode.type.lowercased().contains("qr") || detectedCode.type.lowercased().contains("二维码") {
//
let parsedData = QRCodeParser.parseQRCode(detectedCode.content)
historyItem.qrCodeType = parsedData.type.rawValue
} else {
//
historyItem.barcodeType = detectedCode.type
}
// Core Data
CoreDataManager.shared.addHistoryItem(historyItem)
logInfo("✅ 已创建并保存历史记录项", className: "ScannerView")
return historyItem
}
private func pauseForPreview() {
showPreviewPause = true
//
scannerViewModel.pauseCamera()
}
private func resetToScanning() {
logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView")
// UI
showPreviewPause = false
//
scannerViewModel.resetDetection()
//
resetImageDecodeState()
//
scannerViewModel.resumeCamera()
logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView")
}
private func autoSelectSingleCode(code: DetectedCode) {
logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let totalCodes = self.scannerViewModel.detectedCodes.count + self.decodedImageCodes.count
guard self.showPreviewPause && totalCodes == 1 else {
logInfo("条件不满足,取消自动选择", className: "ScannerView")
return
}
logInfo("条件满足,执行自动选择", className: "ScannerView")
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
struct ScannerView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ScannerView()
}
}
}
#endif