@ -4,6 +4,7 @@ import AudioToolbox
import Combine
import Combine
import CoreData
import CoreData
import QRCode
import QRCode
import Vision
// MARK: - 主 扫 描 视 图
// MARK: - 主 扫 描 视 图
struct ScannerView : View {
struct ScannerView : View {
@ -18,6 +19,8 @@ struct ScannerView: View {
@ State private var showImagePicker = false
@ State private var showImagePicker = false
@ State private var isDecodingImage = false
@ State private var isDecodingImage = false
@ State private var decodedImageCodes : [ DetectedCode ] = [ ]
@ State private var decodedImageCodes : [ DetectedCode ] = [ ]
@ State private var showDecodeFailure = false
@ State private var decodeFailureMessage = " "
var body : some View {
var body : some View {
ZStack {
ZStack {
@ -27,15 +30,18 @@ struct ScannerView: View {
CameraPreviewView ( session : scannerViewModel . captureSession , previewLayer : $ previewLayer )
CameraPreviewView ( session : scannerViewModel . captureSession , previewLayer : $ previewLayer )
. ignoresSafeArea ( )
. ignoresSafeArea ( )
// 扫 描 界 面 覆 盖 层
// 扫 描 界 面 覆 盖 层
ScanningOverlayView (
ScanningOverlayView (
showPreviewPause : showPreviewPause ,
showPreviewPause : showPreviewPause &&
detectedCodesCount : scannerViewModel . detectedCodes . count ,
( ( scannerViewModel . detectedCodes . count > 1 ) || ( decodedImageCodes . count > 1 ) ) ,
onImageDecode : { showImagePicker = true }
detectedCodesCount : scannerViewModel . detectedCodes . count + decodedImageCodes . count ,
)
onImageDecode : { showImagePicker = true }
)
// 条 码 位 置 标 记 覆 盖 层
// 条 码 位 置 标 记 覆 盖 层 - 只 在 需 要 用 户 选 择 时 显 示
if showPreviewPause && ( ! scannerViewModel . detectedCodes . isEmpty || ! decodedImageCodes . isEmpty ) {
if showPreviewPause &&
( ( ! scannerViewModel . detectedCodes . isEmpty && scannerViewModel . detectedCodes . count > 1 ) ||
( ! decodedImageCodes . isEmpty && decodedImageCodes . count > 1 ) ) {
CodePositionOverlay (
CodePositionOverlay (
detectedCodes : scannerViewModel . detectedCodes + decodedImageCodes ,
detectedCodes : scannerViewModel . detectedCodes + decodedImageCodes ,
previewLayer : previewLayer ,
previewLayer : previewLayer ,
@ -43,8 +49,9 @@ struct ScannerView: View {
)
)
}
}
// 测 试 按 钮 ( 调 试 用 )
// 测 试 按 钮 ( 调 试 用 ) - 只 在 需 要 用 户 选 择 时 显 示
if showPreviewPause && ( scannerViewModel . detectedCodes . count + decodedImageCodes . count ) = = 1 {
if showPreviewPause &&
( ( scannerViewModel . detectedCodes . count > 1 ) || ( decodedImageCodes . count > 1 ) ) {
let singleCode = scannerViewModel . detectedCodes . first ? ? decodedImageCodes . first
let singleCode = scannerViewModel . detectedCodes . first ? ? decodedImageCodes . first
if let code = singleCode {
if let code = singleCode {
TestAutoSelectButton (
TestAutoSelectButton (
@ -53,6 +60,16 @@ struct ScannerView: View {
)
)
}
}
}
}
// 解 码 失 败 提 示
if showDecodeFailure {
DecodeFailureOverlay (
message : decodeFailureMessage ,
onDismiss : {
showDecodeFailure = false
}
)
}
} else {
} else {
// 权 限 未 授 权 时 的 U I
// 权 限 未 授 权 时 的 U I
CameraPermissionView (
CameraPermissionView (
@ -287,42 +304,65 @@ struct ScannerView: View {
private func handleImageDecodeResult ( _ image : UIImage ) {
private func handleImageDecodeResult ( _ image : UIImage ) {
isDecodingImage = true
isDecodingImage = true
decodedImageCodes . removeAll ( )
decodedImageCodes . removeAll ( )
showDecodeFailure = false
decodeFailureMessage = " "
logInfo ( " 🔍 开始解码图片 " , className : " ScannerView " )
logInfo ( " 🔍 开始解码图片 " , className : " ScannerView " )
// 在 后 台 线 程 进 行 解 码
// 在 后 台 线 程 进 行 解 码
DispatchQueue . global ( qos : . userInitiated ) . async { [ self ] in
DispatchQueue . global ( qos : . userInitiated ) . async { [ self ] in
var allResults : [ DetectedCode ] = [ ]
// 使 用 Q R C o d e 库 检 测 二 维 码
// 使 用 V i s i o n 框 架 检 测 所 有 条 码 ( 包 括 二 维 码 和 条 形 码 )
let detected = QRCode . DetectQRCodes ( in : image )
if let cgImage = image . cgImage {
if detected . count > 0 {
let barcodeResults = detectBarcodes ( in : cgImage )
logInfo ( " ✅ 检测到 \( detected . count ) 个二维码 " , className : " ScannerView " )
if ! barcodeResults . isEmpty {
logInfo ( " ✅ 检测到 \( barcodeResults . count ) 个条码 " , className : " ScannerView " )
let results = detected . enumerated ( ) . map { index , qrCode in
allResults . append ( contentsOf : barcodeResults )
DetectedCode (
type : " QR Code " ,
content : qrCode . messageString ? ? " 未知内容 " ,
bounds : qrCode . bounds
)
}
}
}
DispatchQueue . main . async {
self . decodedImageCodes = results
// 如 果 没 有 检 测 到 条 码 , 尝 试 使 用 Q R C o d e 库 作 为 备 用 方 案
if allResults . isEmpty {
let detectedQR = QRCode . DetectQRCodes ( in : image )
if detectedQR . count > 0 {
logInfo ( " ✅ 使用QRCode库检测到 \( detectedQR . count ) 个二维码 " , className : " ScannerView " )
let qrResults = detectedQR . enumerated ( ) . map { index , qrCode in
DetectedCode (
type : " QR Code " ,
content : qrCode . messageString ? ? " 未知内容 " ,
bounds : qrCode . bounds
)
}
allResults . append ( contentsOf : qrResults )
}
}
DispatchQueue . main . async {
if ! allResults . isEmpty {
// 去 重 : 移 除 相 同 内 容 的 重 复 条 码
let uniqueResults = self . removeDuplicateCodes ( allResults )
self . decodedImageCodes = uniqueResults
self . isDecodingImage = false
self . isDecodingImage = false
self . pauseForPreview ( )
logInfo ( " ✅ 图片解码完成,去重后共 \( uniqueResults . count ) 个结果 " , className : " ScannerView " )
logInfo ( " ✅ 图片解码完成,共 \( results . count ) 个结果 " , className : " ScannerView " )
// 如 果 只 有 一 个 二 维 码 , 自 动 选 择 并 跳 转
// 图 片 解 码 结 果 直 接 跳 转 , 不 显 示 选 择 点
if results . count = = 1 {
if uniqueResults . count = = 1 {
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.5 ) {
// 直 接 处 理 单 个 条 码 , 跳 转 到 结 果 页
self . handleCodeSelection ( results [ 0 ] )
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.3 ) {
self . handleCodeSelection ( uniqueResults [ 0 ] )
}
}
} else if uniqueResults . count > 1 {
// 多 个 条 码 时 , 显 示 选 择 点 让 用 户 选 择
self . pauseForPreview ( )
logInfo ( " 📱 检测到多个条码,显示选择点 " , className : " ScannerView " )
}
}
}
} else {
} else {
DispatchQueue . main . async {
self . isDecodingImage = false
self . isDecodingImage = false
logWarning ( " ❌ 图片中未检测到二维码 " , className : " ScannerView " )
self . decodeFailureMessage = " 图片中未检测到二维码或条形码 "
self . showDecodeFailure = true
logWarning ( " ❌ 图片中未检测到二维码或条形码 " , className : " ScannerView " )
}
}
}
}
}
}
@ -331,6 +371,119 @@ struct ScannerView: View {
// / 重 置 图 片 解 码 状 态
// / 重 置 图 片 解 码 状 态
private func resetImageDecodeState ( ) {
private func resetImageDecodeState ( ) {
decodedImageCodes . removeAll ( )
decodedImageCodes . removeAll ( )
showDecodeFailure = false
decodeFailureMessage = " "
}
// / 使 用 V i s i o n 框 架 检 测 条 形 码
private func detectBarcodes ( in cgImage : CGImage ) -> [ DetectedCode ] {
let request = VNDetectBarcodesRequest { request , error in
if let error = error {
logWarning ( " 条形码检测错误: \( error . localizedDescription ) " , className : " ScannerView " )
return
}
}
// 设 置 条 形 码 类 型
request . symbologies = [
. ean8 ,
. ean13 ,
. upce ,
. code39 ,
. code39Checksum ,
. code39FullASCII ,
. code39FullASCIIChecksum ,
. code93 ,
. code93i ,
. code128 ,
. itf14 ,
. pdf417 ,
. qr ,
. dataMatrix ,
. aztec
]
let handler = VNImageRequestHandler ( cgImage : cgImage , options : [ : ] )
do {
try handler . perform ( [ request ] )
let results = request . results ? ? [ ]
guard ! results . isEmpty else {
return [ ]
}
return results . enumerated ( ) . map { index , observation in
let barcodeType = getBarcodeTypeString ( from : observation . symbology )
let content = observation . payloadStringValue ? ? " 未知内容 "
logInfo ( " 检测到条形码 # \( index + 1 ) : 类型= \( barcodeType ) , 内容= \( content ) " , className : " ScannerView " )
return DetectedCode (
type : barcodeType ,
content : content ,
bounds : observation . boundingBox
)
}
} catch {
logWarning ( " 条形码检测请求失败: \( error . localizedDescription ) " , className : " ScannerView " )
return [ ]
}
}
// / 获 取 条 形 码 类 型 的 可 读 字 符 串
private func getBarcodeTypeString ( from symbology : VNBarcodeSymbology ) -> String {
switch symbology {
case . ean8 :
return " EAN-8 "
case . ean13 :
return " EAN-13 "
case . upce :
return " UPC-E "
case . code39 :
return " Code 39 "
case . code39Checksum :
return " Code 39 (Checksum) "
case . code39FullASCII :
return " Code 39 (Full ASCII) "
case . code39FullASCIIChecksum :
return " Code 39 (Full ASCII + Checksum) "
case . code93 :
return " Code 93 "
case . code93i :
return " Code 93i "
case . code128 :
return " Code 128 "
case . itf14 :
return " ITF-14 "
case . pdf417 :
return " PDF417 "
case . qr :
return " QR Code "
case . dataMatrix :
return " Data Matrix "
case . aztec :
return " Aztec "
default :
return " Unknown Barcode "
}
}
// / 移 除 重 复 的 条 码 ( 基 于 内 容 去 重 )
private func removeDuplicateCodes ( _ codes : [ DetectedCode ] ) -> [ DetectedCode ] {
var uniqueCodes : [ DetectedCode ] = [ ]
var seenContents : Set < String > = [ ]
for code in codes {
if ! seenContents . contains ( code . content ) {
seenContents . insert ( code . content )
uniqueCodes . append ( code )
} else {
logInfo ( " 🔄 发现重复条码,内容: \( code . content ) ,已跳过 " , className : " ScannerView " )
}
}
return uniqueCodes
}
}
}
}
@ -372,6 +525,82 @@ struct ImagePicker: UIViewControllerRepresentable {
}
}
}
}
// MARK: - 解 码 失 败 提 示 覆 盖 层
struct DecodeFailureOverlay : View {
let message : String
let onDismiss : ( ) -> Void
var body : some View {
ZStack {
// 半 透 明 背 景
Color . black . opacity ( 0.7 )
. ignoresSafeArea ( )
. onTapGesture {
onDismiss ( )
}
// 失 败 提 示 卡 片
VStack ( spacing : 20 ) {
// 失 败 图 标
Image ( systemName : " exclamationmark.triangle.fill " )
. font ( . system ( size : 50 ) )
. foregroundColor ( . orange )
// 失 败 标 题
Text ( " 解码失败 " )
. font ( . title2 )
. fontWeight ( . bold )
. foregroundColor ( . white )
// 失 败 消 息
Text ( message )
. font ( . body )
. foregroundColor ( . white . opacity ( 0.8 ) )
. multilineTextAlignment ( . center )
. padding ( . horizontal , 20 )
// 重 试 按 钮
Button ( action : {
onDismiss ( )
} ) {
HStack ( spacing : 8 ) {
Image ( systemName : " arrow.clockwise " )
. font ( . system ( size : 16 , weight : . semibold ) )
Text ( " 重新选择图片 " )
. font ( . headline )
. fontWeight ( . medium )
}
. foregroundColor ( . white )
. padding ( . horizontal , 20 )
. padding ( . vertical , 12 )
. background (
RoundedRectangle ( cornerRadius : 12 )
. fill ( Color . blue . opacity ( 0.8 ) )
. overlay (
RoundedRectangle ( cornerRadius : 12 )
. stroke ( Color . blue , lineWidth : 1 )
)
)
}
. buttonStyle ( PlainButtonStyle ( ) )
}
. padding ( 30 )
. background (
RoundedRectangle ( cornerRadius : 20 )
. fill ( Color ( . systemGray6 ) . opacity ( 0.9 ) )
. overlay (
RoundedRectangle ( cornerRadius : 20 )
. stroke ( Color . white . opacity ( 0.2 ) , lineWidth : 1 )
)
)
. shadow ( color : . black . opacity ( 0.3 ) , radius : 20 , x : 0 , y : 10 )
}
. zIndex ( 2000 ) // 确 保 在 最 上 层
. transition ( . opacity . combined ( with : . scale ) )
}
}
#if DEBUG
#if DEBUG
struct ScannerView_Previews : PreviewProvider {
struct ScannerView_Previews : PreviewProvider {
static var previews : some View {
static var previews : some View {