@ -24,7 +24,6 @@ struct ScannerView: View {
showPreviewPause : showPreviewPause ,
selectedStyle : $ selectedScanningStyle ,
detectedCodesCount : scannerViewModel . detectedCodes . count ,
onRescan : resetToScanning ,
onClose : { dismiss ( ) }
)
@ -33,7 +32,8 @@ struct ScannerView: View {
CodePositionOverlay (
detectedCodes : scannerViewModel . detectedCodes ,
previewLayer : previewLayer ,
onCodeSelected : handleCodeSelection
onCodeSelected : handleCodeSelection ,
onRescan : resetToScanning
)
}
@ -105,9 +105,22 @@ struct ScannerView: View {
}
private func resetToScanning ( ) {
logInfo ( " 🔄 ScannerView 开始重置到扫描状态 " , className : " ScannerView " )
// 重 置 U I 状 态
showPreviewPause = false
// 重 置 扫 描 状 态 并 重 新 开 始
scannerViewModel . resetDetection ( )
scannerViewModel . startScanning ( )
scannerViewModel . restartScanning ( )
// 延 迟 检 查 会 话 状 态
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 1.0 ) {
logInfo ( " 🔍 检查扫描会话状态 " , className : " ScannerView " )
self . scannerViewModel . checkSessionStatus ( )
}
logInfo ( " ✅ ScannerView 已重置到扫描状态 " , className : " ScannerView " )
}
private func autoSelectSingleCode ( code : DetectedCode ) {
@ -130,7 +143,6 @@ struct ScanningOverlayView: View {
let showPreviewPause : Bool
@ Binding var selectedStyle : ScanningLineStyle
let detectedCodesCount : Int
let onRescan : ( ) -> Void
let onClose : ( ) -> Void
var body : some View {
@ -154,7 +166,6 @@ struct ScanningOverlayView: View {
ScanningBottomButtonsView (
showPreviewPause : showPreviewPause ,
selectedStyle : $ selectedStyle ,
onRescan : onRescan ,
onClose : onClose
)
}
@ -197,7 +208,6 @@ struct ScanningInstructionView: View {
struct ScanningBottomButtonsView : View {
let showPreviewPause : Bool
@ Binding var selectedStyle : ScanningLineStyle
let onRescan : ( ) -> Void
let onClose : ( ) -> Void
var body : some View {
@ -207,26 +217,16 @@ struct ScanningBottomButtonsView: View {
ScanningStyleSelectorView ( selectedStyle : $ selectedStyle )
}
if showPreviewPause {
// 预 览 暂 停 时 的 按 钮
Button ( " rescan _button" . localized ) {
on Rescan ( )
// 关 闭 按 钮 - 只 在 非 预 览 选 择 状 态 时 显 示
if ! showPreviewPause {
Button ( " close _button" . localized ) {
on Close ( )
}
. foregroundColor ( . white )
. padding ( . horizontal , 20 )
. padding ( . vertical , 10 )
. background ( Color . blue )
. cornerRadius ( 20 )
}
// 关 闭 按 钮
Button ( " close_button " . localized ) {
onClose ( )
. padding ( )
. background ( Color . black . opacity ( 0.6 ) )
. cornerRadius ( 10 )
}
. foregroundColor ( . white )
. padding ( )
. background ( Color . black . opacity ( 0.6 ) )
. cornerRadius ( 10 )
}
. padding ( . bottom , 50 )
}
@ -362,23 +362,117 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec
// MARK: - 扫 描 控 制
func startScanning ( ) {
DispatchQueue . global ( qos : . background ) . async { [ weak self ] in
self ? . captureSession ? . startRunning ( )
logInfo ( " 🔄 开始扫描 " , className : " ScannerViewModel " )
// 检 查 会 话 是 否 已 经 在 运 行
if captureSession ? . isRunning = = true {
logInfo ( " ℹ ️ 扫描会话已经在运行" , className : " ScannerViewModel " )
return
}
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in
guard let self = self else { return }
self . captureSession ? . startRunning ( )
// 检 查 启 动 状 态
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.2 ) {
if self . captureSession ? . isRunning = = true {
logInfo ( " ✅ 扫描会话启动成功 " , className : " ScannerViewModel " )
} else {
logWarning ( " ⚠️ 扫描会话启动失败 " , className : " ScannerViewModel " )
}
}
}
}
func stopScanning ( ) {
DispatchQueue . global ( qos : . background ) . async { [ weak self ] in
self ? . captureSession ? . stopRunning ( )
logInfo ( " 🔄 停止扫描 " , className : " ScannerViewModel " )
// 检 查 会 话 是 否 在 运 行
if captureSession ? . isRunning = = true {
DispatchQueue . global ( qos : . userInitiated ) . async { [ weak self ] in
self ? . captureSession ? . stopRunning ( )
DispatchQueue . main . async {
logInfo ( " ✅ 扫描会话已停止 " , className : " ScannerViewModel " )
}
}
} else {
logInfo ( " ℹ ️ 扫描会话已经停止" , className : " ScannerViewModel " )
}
}
func resetDetection ( ) {
DispatchQueue . main . async {
logInfo ( " 🔄 重置检测状态,清空 detectedCodes " , className : " ScannerViewModel " )
self . detectedCodes = [ ]
}
}
func isSessionRunning ( ) -> Bool {
return captureSession ? . isRunning = = true
}
func checkSessionStatus ( ) {
let isRunning = captureSession ? . isRunning = = true
logInfo ( " 📊 扫描会话状态检查: \( isRunning ? " 运行中 " : " 已停止 " ) " , className : " ScannerViewModel " )
if ! isRunning {
logWarning ( " ⚠️ 扫描会话未运行,尝试重新启动 " , className : " ScannerViewModel " )
startScanning ( )
}
}
func restartScanning ( ) {
logInfo ( " 🔄 重新开始扫描 " , className : " ScannerViewModel " )
// 确 保 在 主 线 程 执 行 U I 相 关 操 作
DispatchQueue . main . async { [ weak self ] in
guard let self = self else { return }
// 先 停 止 当 前 会 话
if self . captureSession ? . isRunning = = true {
logInfo ( " 🔄 停止当前运行的扫描会话 " , className : " ScannerViewModel " )
self . captureSession ? . stopRunning ( )
}
// 重 置 检 测 状 态
self . detectedCodes = [ ]
// 延 迟 后 重 新 启 动 会 话
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.5 ) {
logInfo ( " 🔄 准备重新启动扫描会话 " , className : " ScannerViewModel " )
// 在 后 台 线 程 启 动 会 话
DispatchQueue . global ( qos : . userInitiated ) . async {
self . captureSession ? . startRunning ( )
// 检 查 会 话 状 态
DispatchQueue . main . asyncAfter ( deadline : . now ( ) + 0.2 ) {
if self . captureSession ? . isRunning = = true {
logInfo ( " ✅ 扫描会话已成功重新启动 " , className : " ScannerViewModel " )
} else {
logWarning ( " ⚠️ 扫描会话启动失败,尝试重新启动 " , className : " ScannerViewModel " )
// 如 果 启 动 失 败 , 再 次 尝 试
DispatchQueue . global ( qos : . userInitiated ) . async {
self . captureSession ? . startRunning ( )
DispatchQueue . main . async {
if self . captureSession ? . isRunning = = true {
logInfo ( " ✅ 扫描会话第二次尝试启动成功 " , className : " ScannerViewModel " )
} else {
logError ( " ❌ 扫描会话启动失败 " , className : " ScannerViewModel " )
}
}
}
}
}
}
}
}
}
// MARK: - A V C a p t u r e M e t a d a t a O u t p u t O b j e c t s D e l e g a t e
func metadataOutput ( _ output : AVCaptureMetadataOutput ,
@ -429,6 +523,7 @@ struct CodePositionOverlay: View {
let detectedCodes : [ DetectedCode ]
let previewLayer : AVCaptureVideoPreviewLayer ?
let onCodeSelected : ( DetectedCode ) -> Void
let onRescan : ( ) -> Void
var body : some View {
GeometryReader { geometry in
@ -453,6 +548,61 @@ struct CodePositionOverlay: View {
. opacity ( 0.3 )
}
#endif
// 重 新 扫 描 按 钮 - 放 在 右 上 角
VStack {
HStack {
Spacer ( )
Button ( action : {
logInfo ( " 🔄 用户点击重新扫描按钮 " , className : " CodePositionOverlay " )
// 添 加 触 觉 反 馈
let impactFeedback = UIImpactFeedbackGenerator ( style : . medium )
impactFeedback . impactOccurred ( )
onRescan ( )
} ) {
HStack ( spacing : 8 ) {
Image ( systemName : " arrow.clockwise " )
. font ( . system ( size : 18 , weight : . semibold ) )
. rotationEffect ( . degrees ( 0 ) )
. animation ( . easeInOut ( duration : 0.3 ) , value : true )
Text ( " rescan_button " . localized )
. font ( . system ( size : 15 , weight : . semibold ) )
}
. foregroundColor ( . white )
. padding ( . horizontal , 20 )
. padding ( . vertical , 10 )
. background (
RoundedRectangle ( cornerRadius : 25 )
. fill ( Color . blue . opacity ( 0.9 ) )
. shadow ( color : . black . opacity ( 0.3 ) , radius : 4 , x : 0 , y : 2 )
)
}
. buttonStyle ( RescanButtonStyle ( ) )
. padding ( . trailing , 25 )
. padding ( . top , 25 )
// 调 试 按 钮 : 检 查 会 话 状 态
#if DEBUG
Button ( action : {
logInfo ( " 🔍 调试:检查扫描会话状态 " , className : " CodePositionOverlay " )
// 这 里 可 以 添 加 会 话 状 态 检 查 逻 辑
} ) {
Image ( systemName : " info.circle " )
. font ( . system ( size : 16 , weight : . medium ) )
. foregroundColor ( . yellow )
. padding ( 8 )
. background ( Color . black . opacity ( 0.6 ) )
. clipShape ( Circle ( ) )
}
. padding ( . trailing , 10 )
. padding ( . top , 25 )
#endif
}
Spacer ( )
}
}
}
. allowsHitTesting ( true )
@ -569,6 +719,16 @@ struct CodePositionMarker: View {
}
}
// MARK: - 重 新 扫 描 按 钮 样 式
struct RescanButtonStyle : ButtonStyle {
func makeBody ( configuration : Configuration ) -> some View {
configuration . label
. scaleEffect ( configuration . isPressed ? 0.95 : 1.0 )
. opacity ( configuration . isPressed ? 0.8 : 1.0 )
. animation ( . easeInOut ( duration : 0.1 ) , value : configuration . isPressed )
}
}
// MARK: - 扫 描 线 动 画 修 饰 符
struct ScanningLineModifier : ViewModifier {
@ State private var isAnimating = false