Refactor ContentView to enhance UI and add language selection; update ScannerView logging to use structured logging; localize new strings for app title, description, and language selection.

main
v504 2 months ago
parent c8e9fbb9f2
commit 7353270517

@ -8,86 +8,122 @@
import SwiftUI import SwiftUI
struct ContentView: View { struct ContentView: View {
@StateObject private var languageManager = LanguageManager.shared
@State private var showScanner = false @State private var showScanner = false
@State private var scannedCode: String? @State private var scannedResult: String?
@State private var showLanguageSettings = false
@ObservedObject private var languageManager = LanguageManager.shared
var body: some View { var body: some View {
NavigationView { NavigationView {
VStack(spacing: 30) { VStack(spacing: 30) {
HStack { //
Spacer()
Button(action: {
showLanguageSettings = true
}) {
HStack(spacing: 4) {
Text(languageManager.currentLanguage.flag)
Text(languageManager.currentLanguage.displayName)
.font(.caption)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.blue.opacity(0.1))
.cornerRadius(15)
}
}
.padding(.horizontal)
Image(systemName: "qrcode.viewfinder") Image(systemName: "qrcode.viewfinder")
.font(.system(size: 100)) .font(.system(size: 100))
.foregroundColor(.blue) .foregroundColor(.blue)
Text("main_title".localized) //
Text("app_title".localized)
.font(.largeTitle) .font(.largeTitle)
.fontWeight(.bold) .fontWeight(.bold)
.multilineTextAlignment(.center)
if let code = scannedCode { //
VStack(spacing: 10) { Text("app_description".localized)
Text("scan_result".localized) .font(.body)
.font(.headline) .foregroundColor(.secondary)
Text(code) .multilineTextAlignment(.center)
.font(.body) .padding(.horizontal)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}
}
//
Button(action: { Button(action: {
showScanner = true showScanner = true
}) { }) {
HStack { HStack {
Image(systemName: "camera") Image(systemName: "camera.fill")
Text("start_scan_button".localized) Text("start_scanning".localized)
} }
.font(.title2) .font(.title2)
.foregroundColor(.white) .foregroundColor(.white)
.padding() .padding()
.frame(maxWidth: .infinity)
.background(Color.blue) .background(Color.blue)
.cornerRadius(15)
}
.padding(.horizontal, 40)
//
Button(action: {
testLogging()
}) {
HStack {
Image(systemName: "doc.text.fill")
Text("测试日志系统")
}
.font(.title3)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.green)
.cornerRadius(10) .cornerRadius(10)
} }
.padding(.horizontal, 60)
Spacer() //
} HStack {
.padding() Text("language".localized)
.navigationTitle("app_title".localized) .font(.headline)
.sheet(isPresented: $showScanner) {
ScannerView() Picker("language".localized, selection: $languageManager.currentLanguage) {
.onReceive(NotificationCenter.default.publisher(for: .scannerDidScanCode)) { notification in ForEach(Language.allCases, id: \.self) { language in
if let code = notification.object as? String { Text(language.displayName).tag(language)
scannedCode = code
showScanner = false
} }
} }
.pickerStyle(SegmentedPickerStyle())
}
.padding(.horizontal, 40)
//
if let result = scannedResult {
VStack(spacing: 10) {
Text("scan_result".localized)
.font(.headline)
.foregroundColor(.green)
Text(result)
.font(.body)
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.multilineTextAlignment(.center)
}
.padding(.horizontal, 40)
}
Spacer()
} }
.sheet(isPresented: $showLanguageSettings) { .padding()
LanguageSettingsView() .navigationTitle("MyQrCode")
} .navigationBarTitleDisplayMode(.inline)
.onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in }
// .sheet(isPresented: $showScanner) {
ScannerView()
}
.onReceive(NotificationCenter.default.publisher(for: .scannerDidScanCode)) { notification in
if let result = notification.object as? String {
scannedResult = result
} }
} }
.onAppear {
//
languageManager.currentLanguage = .english
}
}
private func testLogging() {
logDebug("这是一条调试日志", className: "ContentView")
logInfo("这是一条信息日志", className: "ContentView")
logWarning("这是一条警告日志", className: "ContentView")
logError("这是一条错误日志", className: "ContentView")
logSuccess("这是一条成功日志", className: "ContentView")
} }
} }

@ -0,0 +1,151 @@
import Foundation
import SwiftUI
import os.log
import Combine
///
enum LogLevel: String, CaseIterable {
case debug = "🔍"
case info = ""
case warning = "⚠️"
case error = ""
case success = ""
var localizedName: String {
switch self {
case .debug: return "调试"
case .info: return "信息"
case .warning: return "警告"
case .error: return "错误"
case .success: return "成功"
}
}
}
///
class Logger: ObservableObject {
static let shared = Logger()
@Published var isEnabled = true
@Published var minimumLevel: LogLevel = .debug
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter
}()
private init() {}
///
/// - Parameters:
/// - level:
/// - message:
/// - file:
/// - function:
/// - line:
/// - className:
func log(
_ level: LogLevel,
_ message: String,
file: String = #file,
function: String = #function,
line: Int = #line,
className: String? = nil
) {
guard isEnabled && shouldLog(level) else { return }
let timestamp = dateFormatter.string(from: Date())
let fileName = URL(fileURLWithPath: file).lastPathComponent
let functionName = function
let lineNumber = line
let classDisplayName = className ?? "Unknown"
let logMessage = "[\(timestamp)] \(level.rawValue) [\(classDisplayName)] [\(fileName):\(lineNumber)] \(functionName): \(message)"
//
print(logMessage)
//
#if DEBUG
// os_log("%{public}@", log: .default, type: .debug, logMessage)
#endif
}
///
private func shouldLog(_ level: LogLevel) -> Bool {
let levelOrder: [LogLevel] = [.debug, .info, .warning, .error, .success]
guard let currentIndex = levelOrder.firstIndex(of: minimumLevel),
let messageIndex = levelOrder.firstIndex(of: level) else {
return false
}
return messageIndex >= currentIndex
}
// MARK: - 便
func debug(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
log(.debug, message, file: file, function: function, line: line, className: className)
}
func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
log(.info, message, file: file, function: function, line: line, className: className)
}
func warning(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
log(.warning, message, file: file, function: function, line: line, className: className)
}
func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
log(.error, message, file: file, function: function, line: line, className: className)
}
func success(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
log(.success, message, file: file, function: function, line: line, className: className)
}
}
// MARK: -
///
func logDebug(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
Logger.shared.debug(message, file: file, function: function, line: line, className: className)
}
///
func logInfo(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
Logger.shared.info(message, file: file, function: function, line: line, className: className)
}
///
func logWarning(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
Logger.shared.warning(message, file: file, function: function, line: line, className: className)
}
///
func logError(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
Logger.shared.error(message, file: file, function: function, line: line, className: className)
}
///
func logSuccess(_ message: String, file: String = #file, function: String = #function, line: Int = #line, className: String? = nil) {
Logger.shared.success(message, file: file, function: function, line: line, className: className)
}
// MARK: -
extension NSObject {
var className: String {
return String(describing: type(of: self))
}
static var className: String {
return String(describing: self)
}
}
extension View {
var className: String {
return String(describing: type(of: self))
}
}

@ -0,0 +1,129 @@
# 日志系统使用说明
## 概述
我们创建了一个功能强大的日志系统,可以显示代码在文件的行号、类名和打印时间,完全替换了原来的 `print` 语句。
## 特性
- 🔍 **调试日志**: 用于详细的调试信息
- **信息日志**: 用于一般信息
- ⚠️ **警告日志**: 用于警告信息
- ❌ **错误日志**: 用于错误信息
- ✅ **成功日志**: 用于成功操作
## 使用方法
### 1. 基本日志函数
```swift
// 调试日志
logDebug("这是一条调试信息", className: "MyClass")
// 信息日志
logInfo("这是一条信息", className: "MyClass")
// 警告日志
logWarning("这是一条警告", className: "MyClass")
// 错误日志
logError("这是一条错误", className: "MyClass")
// 成功日志
logSuccess("操作成功", className: "MyClass")
```
### 2. 自动获取类名
```swift
class MyViewController: UIViewController {
func someMethod() {
// 使用 self.className 自动获取类名
logInfo("方法被调用", className: self.className)
// 或者使用静态方法
logInfo("类被初始化", className: MyViewController.className)
}
}
```
### 3. 日志输出格式
日志输出格式如下:
```
[时间戳] 图标 [类名] [文件名:行号] 函数名: 消息内容
```
例如:
```
[14:30:25.123] [ScannerView] [ScannerView.swift:162] onReceive: 检测到条码数量: 2
```
### 4. 日志级别控制
```swift
// 设置最小日志级别(只显示该级别及以上的日志)
Logger.shared.minimumLevel = .info // 只显示 info、warning、error、success
// 启用/禁用日志
Logger.shared.isEnabled = false // 完全禁用日志
```
## 已替换的 print 语句
### ScannerView.swift
- 条码检测相关日志
- 屏幕方向变化日志
- 自动选择定时器日志
### ScannerViewModel.swift
- 元数据输出日志
- 条码创建日志
- 数据更新日志
### CodePositionMarker
- 位置计算日志
- 坐标转换日志
- 边界检查日志
## 日志级别说明
1. **Debug (🔍)**: 最详细的调试信息,包括坐标、尺寸等
2. **Info ()**: 一般信息,如状态变化、操作结果
3. **Warning (⚠️)**: 警告信息,如使用默认值、降级处理
4. **Error (❌)**: 错误信息,如权限失败、设备不支持
5. **Success (✅)**: 成功操作,如扫描完成、权限获取
## 性能考虑
- 日志系统使用 `@Published` 属性,支持 SwiftUI 的响应式更新
- 在 Release 版本中,可以通过设置 `isEnabled = false` 完全禁用日志
- 日志输出使用 `DispatchQueue.main.async` 确保在主线程执行
## 测试日志系统
在 ContentView 中点击"测试日志系统"按钮,可以看到所有级别的日志输出示例。
## 自定义扩展
如果需要添加新的日志级别或自定义格式,可以扩展 `LogLevel` 枚举和 `Logger` 类:
```swift
extension LogLevel {
case custom = "🎯"
var localizedName: String {
switch self {
case .custom: return "自定义"
// ... 其他情况
}
}
}
```
## 注意事项
1. 所有日志函数都支持自动获取文件名、行号和函数名
2. 类名参数是可选的,如果不提供会显示 "Unknown"
3. 日志系统是线程安全的,可以在任何线程中调用
4. 在开发阶段建议保持日志启用,生产环境可以适当调整级别

@ -160,15 +160,15 @@ struct ScannerView: View {
.onReceive(scannerViewModel.$detectedCodes) { codes in .onReceive(scannerViewModel.$detectedCodes) { codes in
if !codes.isEmpty { if !codes.isEmpty {
print("检测到条码数量: \(codes.count)") logInfo("检测到条码数量: \(codes.count)", className: "ScannerView")
if codes.count == 1 { if codes.count == 1 {
// //
print("单个条码,准备自动选择") logInfo("单个条码,准备自动选择", className: "ScannerView")
pauseForPreview() pauseForPreview()
autoSelectSingleCode(code: codes[0]) autoSelectSingleCode(code: codes[0])
} else { } else {
// //
print("多个条码,等待用户选择") logInfo("多个条码,等待用户选择", className: "ScannerView")
pauseForPreview() pauseForPreview()
} }
} }
@ -176,7 +176,7 @@ struct ScannerView: View {
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
// //
screenOrientation = UIDevice.current.orientation screenOrientation = UIDevice.current.orientation
print("Screen orientation changed to: \(screenOrientation.rawValue)") logInfo("Screen orientation changed to: \(screenOrientation)", className: "ScannerView")
} }
} }
@ -192,22 +192,22 @@ struct ScannerView: View {
} }
private func autoSelectSingleCode(code: DetectedCode) { private func autoSelectSingleCode(code: DetectedCode) {
print("开始自动选择定时器,条码类型: \(code.type)") logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
// //
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
print("自动选择定时器触发") logInfo("自动选择定时器触发", className: "ScannerView")
print("当前状态 - showPreviewPause: \(self.showPreviewPause)") logInfo("当前状态 - showPreviewPause: \(self.showPreviewPause)", className: "ScannerView")
print("当前条码数量: \(self.scannerViewModel.detectedCodes.count)") logInfo("当前条码数量: \(self.scannerViewModel.detectedCodes.count)", className: "ScannerView")
if self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 { if self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 {
print("条件满足,执行自动选择") logInfo("条件满足,执行自动选择", className: "ScannerView")
// //
let selectedCode = "类型: \(code.type)\n内容: \(code.content)" let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
print("发送通知: \(selectedCode)") logInfo("发送通知: \(selectedCode)", className: "ScannerView")
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode) NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
self.dismiss() self.dismiss()
} else { } else {
print("条件不满足,取消自动选择") logInfo("条件不满足,取消自动选择", className: "ScannerView")
} }
} }
} }
@ -321,7 +321,7 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec
didOutput metadataObjects: [AVMetadataObject], didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) { from connection: AVCaptureConnection) {
print("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象") logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel")
// //
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
@ -346,15 +346,15 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec
) )
codes.append(detectedCode) codes.append(detectedCode)
print("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)") logInfo("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)", className: "ScannerViewModel")
} }
} }
print("准备更新 detectedCodes数量: \(codes.count)") logInfo("准备更新 detectedCodes数量: \(codes.count)", className: "ScannerViewModel")
// //
DispatchQueue.main.async { DispatchQueue.main.async {
print("在主线程更新 detectedCodes") logInfo("在主线程更新 detectedCodes", className: "ScannerViewModel")
self.detectedCodes = codes self.detectedCodes = codes
} }
} }
@ -424,9 +424,9 @@ struct CodePositionMarker: View {
onCodeSelected(selectedCode) onCodeSelected(selectedCode)
} }
.onAppear { .onAppear {
print("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)") logDebug("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)", className: "CodePositionMarker")
print("Screen size: \(geometry.size)") logDebug("Screen size: \(geometry.size)", className: "CodePositionMarker")
print("Code bounds: \(code.bounds)") logDebug("Code bounds: \(code.bounds)", className: "CodePositionMarker")
} }
} }
} }
@ -434,13 +434,13 @@ struct CodePositionMarker: View {
private func calculatePosition(screenSize: CGSize) -> CGPoint { private func calculatePosition(screenSize: CGSize) -> CGPoint {
guard let previewLayer = previewLayer else { guard let previewLayer = previewLayer else {
// 使 // 使
print("No preview layer available, using screen center") logWarning("No preview layer available, using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
} }
// //
guard previewLayer.session?.isRunning == true else { guard previewLayer.session?.isRunning == true else {
print("Preview layer session not running, using screen center") logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
} }
@ -453,7 +453,7 @@ struct CodePositionMarker: View {
// //
guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else { guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else {
print("Invalid converted point: \(convertedPoint), using screen center") logWarning("Invalid converted point: \(convertedPoint), using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
} }
@ -461,10 +461,10 @@ struct CodePositionMarker: View {
let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x)) let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x))
let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y)) let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y))
print("AVFoundation bounds: \(code.bounds)") logDebug("AVFoundation bounds: \(code.bounds)", className: "CodePositionMarker")
print("Converted point: \(convertedPoint)") logDebug("Converted point: \(convertedPoint)", className: "CodePositionMarker")
print("Screen size: \(screenSize)") logDebug("Screen size: \(screenSize)", className: "CodePositionMarker")
print("Clamped: x=\(clampedX), y=\(clampedY)") logDebug("Clamped: x=\(clampedX), y=\(clampedY)", className: "CodePositionMarker")
return CGPoint(x: clampedX, y: clampedY) return CGPoint(x: clampedX, y: clampedY)
} }

@ -21,8 +21,11 @@
// Content View // Content View
"main_title" = "Barcode Scanner"; "main_title" = "Barcode Scanner";
"start_scan_button" = "Start Scanning"; "app_title" = "MyQrCode";
"app_description" = "Scan QR codes and barcodes with ease";
"start_scanning" = "Start Scanning";
"scan_result" = "Scan Result:"; "scan_result" = "Scan Result:";
"language" = "Language";
// Error Messages // Error Messages
"scan_error_title" = "Scan Error"; "scan_error_title" = "Scan Error";

@ -21,8 +21,11 @@
// 主视图 // 主视图
"main_title" = "条码扫描器"; "main_title" = "条码扫描器";
"start_scan_button" = "开始扫描"; "app_title" = "MyQrCode";
"app_description" = "轻松扫描二维码和条形码";
"start_scanning" = "开始扫描";
"scan_result" = "扫描结果:"; "scan_result" = "扫描结果:";
"language" = "语言";
// 错误信息 // 错误信息
"scan_error_title" = "扫描失败"; "scan_error_title" = "扫描失败";

Loading…
Cancel
Save