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.

433 lines
15 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import SwiftUI
import AVFoundation
import AudioToolbox
import Combine
// MARK: -
class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
@Published var detectedCodes: [DetectedCode] = []
@Published var showAlert = false
@Published var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
@Published var showPermissionAlert = false
@Published var isTorchOn = false
var captureSession: AVCaptureSession!
private var metadataOutput: AVCaptureMetadataOutput?
private var videoDevice: AVCaptureDevice?
private var isProcessingDetection = false //
override init() {
super.init()
checkCameraPermission()
}
// MARK: -
private func checkCameraPermission() {
logInfo("🔍 检查相机权限状态", className: "ScannerViewModel")
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
logInfo("✅ 相机权限已授权", className: "ScannerViewModel")
cameraAuthorizationStatus = .authorized
setupCaptureSession()
case .notDetermined:
logInfo("❓ 相机权限未确定,请求权限", className: "ScannerViewModel")
requestCameraPermission()
case .denied, .restricted:
logWarning("❌ 相机权限被拒绝或受限", className: "ScannerViewModel")
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
showPermissionAlert = true
@unknown default:
logWarning("❓ 未知的相机权限状态", className: "ScannerViewModel")
cameraAuthorizationStatus = .notDetermined
}
}
private func requestCameraPermission() {
logInfo("🔐 请求相机权限", className: "ScannerViewModel")
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
if granted {
logInfo("✅ 相机权限请求成功", className: "ScannerViewModel")
self?.cameraAuthorizationStatus = .authorized
self?.setupCaptureSession()
} else {
logWarning("❌ 相机权限请求被拒绝", className: "ScannerViewModel")
self?.cameraAuthorizationStatus = .denied
self?.showPermissionAlert = true
}
}
}
}
func openSettings() {
logInfo("⚙️ 打开系统设置", className: "ScannerViewModel")
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl) { success in
if success {
logInfo("✅ 成功打开系统设置", className: "ScannerViewModel")
} else {
logWarning("⚠️ 打开系统设置失败", className: "ScannerViewModel")
}
}
}
}
func refreshCameraPermission() {
logInfo("🔍 重新检查相机权限状态", className: "ScannerViewModel")
checkCameraPermission()
}
// MARK: -
private func setupCaptureSession() {
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
showAlert = true
return
}
//
videoDevice = videoCaptureDevice
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
showAlert = true
return
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
} else {
showAlert = true
return
}
metadataOutput = AVCaptureMetadataOutput()
if let metadataOutput = metadataOutput,
captureSession.canAddOutput(metadataOutput) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .code128, .code39, .upce, .pdf417, .aztec]
} else {
showAlert = true
return
}
}
// MARK: -
func startScanning() {
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() {
logInfo("🔄 停止扫描", className: "ScannerViewModel")
//
isProcessingDetection = true
//
if captureSession?.isRunning == true {
//
captureSession?.stopRunning()
logInfo("✅ 扫描会话已停止", className: "ScannerViewModel")
} else {
logInfo(" 扫描会话已经停止", className: "ScannerViewModel")
}
}
///
func pauseCamera() {
logInfo("⏸️ 暂停相机功能", className: "ScannerViewModel")
//
isProcessingDetection = true
//
if captureSession?.isRunning == true {
captureSession?.stopRunning()
logInfo("✅ 相机会话已暂停", className: "ScannerViewModel")
} else {
logInfo(" 相机会话已经停止", className: "ScannerViewModel")
}
}
///
func resumeCamera() {
logInfo("▶️ 恢复相机功能", className: "ScannerViewModel")
//
guard cameraAuthorizationStatus == .authorized else {
logWarning("❌ 相机权限未授权,无法恢复相机", className: "ScannerViewModel")
return
}
//
if captureSession == nil || captureSession.inputs.isEmpty || captureSession.outputs.isEmpty {
logInfo("🔄 重新设置相机会话", className: "ScannerViewModel")
setupCaptureSession()
}
//
isProcessingDetection = false
//
if captureSession?.isRunning != true {
logInfo("🚀 启动相机会话", className: "ScannerViewModel")
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession?.startRunning()
DispatchQueue.main.async {
if self?.captureSession?.isRunning == true {
logInfo("✅ 相机会话启动成功", className: "ScannerViewModel")
} else {
logWarning("⚠️ 相机会话启动失败", className: "ScannerViewModel")
}
}
}
}
logInfo("✅ 相机功能已恢复", className: "ScannerViewModel")
}
func resetDetection() {
DispatchQueue.main.async {
logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel")
self.detectedCodes = []
self.isProcessingDetection = false //
}
}
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")
//
if captureSession?.isRunning == true {
logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel")
captureSession?.stopRunning()
}
//
resetDetection()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel")
// 线
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession?.startRunning()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self?.captureSession?.isRunning == true {
logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel")
} else {
logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel")
}
}
}
}
}
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
//
guard !isProcessingDetection else {
logInfo("⚠️ 正在处理检测结果,忽略新的检测", className: "ScannerViewModel")
return
}
logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel")
//
isProcessingDetection = true
//
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
//
stopScanning()
//
var codes: [DetectedCode] = []
for metadataObject in metadataObjects {
if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue {
let codeType = getBarcodeTypeString(from: readableObject.type)
let bounds = readableObject.bounds
let detectedCode = DetectedCode(
type: codeType,
content: stringValue,
bounds: bounds,
source: .camera
)
codes.append(detectedCode)
logInfo("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)", className: "ScannerViewModel")
}
}
logInfo("准备更新 detectedCodes数量: \(codes.count)", className: "ScannerViewModel")
//
DispatchQueue.main.async {
logInfo("在主线程更新 detectedCodes", className: "ScannerViewModel")
self.detectedCodes = codes
}
}
// MARK: -
///
var isTorchAvailable: Bool {
guard let device = videoDevice else { return false }
return device.hasTorch && device.isTorchAvailable
}
///
func toggleTorch() {
guard let device = videoDevice else {
logWarning("❌ 没有可用的视频设备", className: "ScannerViewModel")
return
}
guard device.hasTorch && device.isTorchAvailable else {
logWarning("❌ 设备不支持手电筒", className: "ScannerViewModel")
return
}
do {
try device.lockForConfiguration()
if isTorchOn {
//
device.torchMode = .off
isTorchOn = false
logInfo("🔦 手电筒已关闭", className: "ScannerViewModel")
} else {
//
try device.setTorchModeOn(level: 1.0)
isTorchOn = true
logInfo("🔦 手电筒已打开", className: "ScannerViewModel")
}
device.unlockForConfiguration()
} catch {
logError("❌ 手电筒控制失败: \(error.localizedDescription)", className: "ScannerViewModel")
device.unlockForConfiguration()
}
}
///
func turnOffTorch() {
guard let device = videoDevice else { return }
do {
try device.lockForConfiguration()
device.torchMode = .off
isTorchOn = false
device.unlockForConfiguration()
logInfo("🔦 手电筒已关闭", className: "ScannerViewModel")
} catch {
logError("❌ 关闭手电筒失败: \(error.localizedDescription)", className: "ScannerViewModel")
device.unlockForConfiguration()
}
}
// MARK: -
///
private func getBarcodeTypeString(from metadataType: AVMetadataObject.ObjectType) -> String {
switch metadataType {
case .ean8:
return "EAN-8"
case .ean13:
return "EAN-13"
case .upce:
return "UPC-E"
case .code39:
return "Code 39"
case .code93:
return "Code 93"
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:
//
let typeString = metadataType.rawValue
if typeString.contains("org.gs1.") {
// org.gs1.
let cleanType = typeString.replacingOccurrences(of: "org.gs1.", with: "")
return cleanType.uppercased()
}
return typeString
}
}
}