@ -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 {
// 权 限 未 授 权 时 的 U I
// 权 限 未 授 权 时 的 U I
@ -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
// 使 用 Q R C o d e 库 检 测 二 维 码
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: - 图 片 选 择 器 ( 兼 容 i O S 1 5 )
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