Add flashlight control to ScannerView; implement toggle functionality and ensure torch is off on exit. Enhance user experience with haptic feedback on button press.

main
v504 2 months ago
parent d929fd046a
commit 89fb4a2daa

@ -59,6 +59,25 @@ struct ScannerView: View {
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(false)
.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 {
@ -91,6 +110,10 @@ struct ScannerView: View {
}
.onDisappear {
scannerViewModel.stopScanning()
// 退
if scannerViewModel.isTorchOn {
scannerViewModel.turnOffTorch()
}
}
.alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) {
Button("OK") { }

@ -9,9 +9,11 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec
@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?
override init() {
super.init()
@ -91,6 +93,9 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec
return
}
//
videoDevice = videoCaptureDevice
let videoInput: AVCaptureDeviceInput
do {
@ -277,4 +282,63 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec
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()
}
}
}

@ -0,0 +1,288 @@
# 手电筒功能添加说明
## 🎯 功能概述
`ScannerView` 添加了手电筒控制功能,用户可以通过工具栏按钮打开/关闭手电筒,提升在低光环境下的扫描体验。
## 🔧 主要修改内容
### 1. **ScannerViewModel.swift 的修改**
#### 新增状态变量
```swift
@Published var isTorchOn = false // 手电筒开关状态
private var videoDevice: AVCaptureDevice? // 视频设备引用
```
#### 保存视频设备引用
```swift
private func setupCaptureSession() {
// ... 现有代码 ...
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
showAlert = true
return
}
// 保存视频设备引用,用于手电筒控制
videoDevice = videoCaptureDevice
// ... 现有代码 ...
}
```
#### 手电筒控制方法
```swift
// 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 {
// 关闭手电筒
try device.setTorchModeOn(level: 0.0)
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()
try device.setTorchModeOn(level: 0.0)
isTorchOn = false
device.unlockForConfiguration()
logInfo("🔦 手电筒已关闭", className: "ScannerViewModel")
} catch {
logError("❌ 关闭手电筒失败: \(error.localizedDescription)", className: "ScannerViewModel")
device.unlockForConfiguration()
}
}
```
### 2. **ScannerView.swift 的修改**
#### 工具栏布局更新
```swift
.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 {
// ... 现有代码 ...
}
}
}
```
#### 生命周期管理
```swift
.onDisappear {
scannerViewModel.stopScanning()
// 确保退出时关闭手电筒
if scannerViewModel.isTorchOn {
scannerViewModel.turnOffTorch()
}
}
```
## 🚀 功能特性
### 1. **智能显示**
- 手电筒按钮只在设备支持手电筒时显示
- 只在相机权限已授权时显示
- 避免在不支持的设备上显示无效按钮
### 2. **状态指示**
- 使用闪电图标表示手电筒状态:
- `bolt`: 手电筒关闭状态(蓝色)
- `bolt.fill`: 手电筒开启状态(黄色)
- 颜色变化提供直观的状态反馈
### 3. **安全控制**
- 设备配置锁定/解锁确保安全
- 错误处理和日志记录
- 应用退出时自动关闭手电筒
### 4. **用户体验**
- 触觉反馈增强交互体验
- 按钮位置合理(导航栏左侧)
- 图标大小和样式符合系统设计
## 📱 界面布局
### 工具栏布局
```
[🔦 手电筒] ← 扫描器 → [🔄 重新扫描]
```
- **左侧**: 手电筒按钮(始终显示,如果支持)
- **中间**: 导航标题
- **右侧**: 重新扫描按钮(仅在预览暂停时显示)
### 按钮状态
- **关闭状态**: 蓝色闪电图标 + 关闭填充
- **开启状态**: 黄色闪电图标 + 开启填充
## 🧪 测试要点
### 1. **功能测试**
- ✅ 手电筒按钮在支持的设备上正确显示
- ✅ 点击按钮正确切换手电筒状态
- ✅ 手电筒实际开启/关闭
- ✅ 状态指示正确更新
### 2. **兼容性测试**
- ✅ 在不支持手电筒的设备上不显示按钮
- ✅ 在相机权限未授权时不显示按钮
- ✅ 在不同设备上正常工作
### 3. **安全性测试**
- ✅ 应用退出时手电筒自动关闭
- ✅ 设备配置正确锁定/解锁
- ✅ 错误情况下的安全处理
### 4. **用户体验测试**
- ✅ 触觉反馈正常工作
- ✅ 按钮响应及时
- ✅ 图标和颜色变化清晰
## 🔍 技术实现细节
### 1. **AVFoundation 集成**
- 使用 `AVCaptureDevice` 控制手电筒
- 设备配置锁定确保安全操作
- 错误处理和资源管理
### 2. **状态管理**
- `@Published var isTorchOn` 提供响应式状态
- 状态变化自动更新 UI
- 生命周期管理确保状态一致性
### 3. **权限检查**
- 检查设备硬件支持
- 检查相机权限状态
- 条件渲染避免无效操作
### 4. **错误处理**
- 完整的错误捕获和日志记录
- 设备配置解锁确保资源释放
- 用户友好的错误提示
## 🚨 注意事项
### 1. **设备兼容性**
- 不是所有设备都支持手电筒
- 需要检查 `hasTorch``isTorchAvailable` 属性
- 在模拟器上可能无法测试手电筒功能
### 2. **权限管理**
- 手电筒功能需要相机权限
- 权限变化时按钮状态需要更新
- 权限被拒绝时的处理
### 3. **电池管理**
- 手电筒会消耗较多电量
- 建议在不需要时及时关闭
- 应用退出时自动关闭
### 4. **用户体验**
- 手电筒开启时提供视觉反馈
- 考虑添加亮度调节功能
- 在低光环境下的使用提示
## 🐛 Bug修复
### 问题描述
初始实现中使用 `setTorchModeOn(level: 0.0)` 来关闭手电筒,这会导致崩溃:
```
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[AVCaptureDevice setTorchModeOnWithLevel:error:] The passed torchLevel 0.000000 is invalid'
```
### 修复方案
```swift
// ❌ 错误的关闭方式
try device.setTorchModeOn(level: 0.0)
// ✅ 正确的关闭方式
device.torchMode = .off
```
### 修复后的代码
```swift
if isTorchOn {
// 关闭手电筒
device.torchMode = .off
isTorchOn = false
logInfo("🔦 手电筒已关闭", className: "ScannerViewModel")
} else {
// 打开手电筒
try device.setTorchModeOn(level: 1.0)
isTorchOn = true
logInfo("🔦 手电筒已打开", className: "ScannerViewModel")
}
```
## 📊 功能总结
通过这次修改,我们成功添加了手电筒控制功能:
1. **功能完整**: 支持手电筒的开启、关闭和状态切换
2. **界面友好**: 直观的图标和颜色状态指示
3. **安全可靠**: 完整的错误处理和资源管理
4. **用户体验**: 触觉反馈和智能显示逻辑
5. **稳定性**: 修复了关闭手电筒时的崩溃bug
手电筒功能将显著提升用户在低光环境下的扫描体验,特别是在夜间或光线不足的室内环境中。🎉
Loading…
Cancel
Save