diff --git a/MyQrCode/ContentView.swift b/MyQrCode/ContentView.swift index 409f0fd..d550e9a 100644 --- a/MyQrCode/ContentView.swift +++ b/MyQrCode/ContentView.swift @@ -8,86 +8,122 @@ import SwiftUI struct ContentView: View { + @StateObject private var languageManager = LanguageManager.shared @State private var showScanner = false - @State private var scannedCode: String? - @State private var showLanguageSettings = false - @ObservedObject private var languageManager = LanguageManager.shared + @State private var scannedResult: String? var body: some View { NavigationView { 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") .font(.system(size: 100)) .foregroundColor(.blue) - Text("main_title".localized) + // 标题 + Text("app_title".localized) .font(.largeTitle) .fontWeight(.bold) + .multilineTextAlignment(.center) - if let code = scannedCode { - VStack(spacing: 10) { - Text("scan_result".localized) - .font(.headline) - Text(code) - .font(.body) - .padding() - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - } - } + // 描述 + Text("app_description".localized) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + // 扫描按钮 Button(action: { showScanner = true }) { HStack { - Image(systemName: "camera") - Text("start_scan_button".localized) + Image(systemName: "camera.fill") + Text("start_scanning".localized) } .font(.title2) .foregroundColor(.white) .padding() + .frame(maxWidth: .infinity) .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) } + .padding(.horizontal, 60) - Spacer() - } - .padding() - .navigationTitle("app_title".localized) - .sheet(isPresented: $showScanner) { - ScannerView() - .onReceive(NotificationCenter.default.publisher(for: .scannerDidScanCode)) { notification in - if let code = notification.object as? String { - scannedCode = code - showScanner = false + // 语言选择器 + HStack { + Text("language".localized) + .font(.headline) + + Picker("language".localized, selection: $languageManager.currentLanguage) { + ForEach(Language.allCases, id: \.self) { language in + Text(language.displayName).tag(language) } } + .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) { - LanguageSettingsView() - } - .onReceive(NotificationCenter.default.publisher(for: .languageChanged)) { _ in - // 语言变化时刷新界面 + .padding() + .navigationTitle("MyQrCode") + .navigationBarTitleDisplayMode(.inline) + } + .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") } } diff --git a/MyQrCode/Logger.swift b/MyQrCode/Logger.swift new file mode 100644 index 0000000..d8f4a7f --- /dev/null +++ b/MyQrCode/Logger.swift @@ -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)) + } +} diff --git a/MyQrCode/README_Logger.md b/MyQrCode/README_Logger.md new file mode 100644 index 0000000..8b4aea0 --- /dev/null +++ b/MyQrCode/README_Logger.md @@ -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. 在开发阶段建议保持日志启用,生产环境可以适当调整级别 \ No newline at end of file diff --git a/MyQrCode/ScannerView.swift b/MyQrCode/ScannerView.swift index 7c289ec..293d522 100644 --- a/MyQrCode/ScannerView.swift +++ b/MyQrCode/ScannerView.swift @@ -160,15 +160,15 @@ struct ScannerView: View { .onReceive(scannerViewModel.$detectedCodes) { codes in if !codes.isEmpty { - print("检测到条码数量: \(codes.count)") + logInfo("检测到条码数量: \(codes.count)", className: "ScannerView") if codes.count == 1 { // 只有一个码,显示标记后一秒自动显示结果 - print("单个条码,准备自动选择") + logInfo("单个条码,准备自动选择", className: "ScannerView") pauseForPreview() autoSelectSingleCode(code: codes[0]) } else { // 多个码,暂停预览并引导用户选择 - print("多个条码,等待用户选择") + logInfo("多个条码,等待用户选择", className: "ScannerView") pauseForPreview() } } @@ -176,7 +176,7 @@ struct ScannerView: View { .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in // 屏幕方向变化时更新状态 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) { - print("开始自动选择定时器,条码类型: \(code.type)") + logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView") // 一秒后自动选择单个条码 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - print("自动选择定时器触发") - print("当前状态 - showPreviewPause: \(self.showPreviewPause)") - print("当前条码数量: \(self.scannerViewModel.detectedCodes.count)") + logInfo("自动选择定时器触发", className: "ScannerView") + logInfo("当前状态 - showPreviewPause: \(self.showPreviewPause)", className: "ScannerView") + logInfo("当前条码数量: \(self.scannerViewModel.detectedCodes.count)", className: "ScannerView") if self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 { - print("条件满足,执行自动选择") + logInfo("条件满足,执行自动选择", className: "ScannerView") // 确保仍然只有一个条码且处于预览暂停状态 let selectedCode = "类型: \(code.type)\n内容: \(code.content)" - print("发送通知: \(selectedCode)") + logInfo("发送通知: \(selectedCode)", className: "ScannerView") NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode) self.dismiss() } else { - print("条件不满足,取消自动选择") + logInfo("条件不满足,取消自动选择", className: "ScannerView") } } } @@ -321,7 +321,7 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { - print("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象") + logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel") // 震动反馈 AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) @@ -346,15 +346,15 @@ class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjec ) 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 { - print("在主线程更新 detectedCodes") + logInfo("在主线程更新 detectedCodes", className: "ScannerViewModel") self.detectedCodes = codes } } @@ -424,9 +424,9 @@ struct CodePositionMarker: View { onCodeSelected(selectedCode) } .onAppear { - print("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)") - print("Screen size: \(geometry.size)") - print("Code bounds: \(code.bounds)") + logDebug("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)", className: "CodePositionMarker") + logDebug("Screen size: \(geometry.size)", className: "CodePositionMarker") + logDebug("Code bounds: \(code.bounds)", className: "CodePositionMarker") } } } @@ -434,13 +434,13 @@ struct CodePositionMarker: View { private func calculatePosition(screenSize: CGSize) -> CGPoint { 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) } // 检查预览层是否有效 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) } @@ -453,7 +453,7 @@ struct CodePositionMarker: View { // 验证转换结果是否有效 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) } @@ -461,10 +461,10 @@ struct CodePositionMarker: View { let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x)) let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y)) - print("AVFoundation bounds: \(code.bounds)") - print("Converted point: \(convertedPoint)") - print("Screen size: \(screenSize)") - print("Clamped: x=\(clampedX), y=\(clampedY)") + logDebug("AVFoundation bounds: \(code.bounds)", className: "CodePositionMarker") + logDebug("Converted point: \(convertedPoint)", className: "CodePositionMarker") + logDebug("Screen size: \(screenSize)", className: "CodePositionMarker") + logDebug("Clamped: x=\(clampedX), y=\(clampedY)", className: "CodePositionMarker") return CGPoint(x: clampedX, y: clampedY) } diff --git a/MyQrCode/en.lproj/Localizable.strings b/MyQrCode/en.lproj/Localizable.strings index 737127f..d7f1cfe 100644 --- a/MyQrCode/en.lproj/Localizable.strings +++ b/MyQrCode/en.lproj/Localizable.strings @@ -21,8 +21,11 @@ // Content View "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:"; +"language" = "Language"; // Error Messages "scan_error_title" = "Scan Error"; diff --git a/MyQrCode/zh-Hans.lproj/Localizable.strings b/MyQrCode/zh-Hans.lproj/Localizable.strings index 16e78bc..9b72c84 100644 --- a/MyQrCode/zh-Hans.lproj/Localizable.strings +++ b/MyQrCode/zh-Hans.lproj/Localizable.strings @@ -21,8 +21,11 @@ // 主视图 "main_title" = "条码扫描器"; -"start_scan_button" = "开始扫描"; +"app_title" = "MyQrCode"; +"app_description" = "轻松扫描二维码和条形码"; +"start_scanning" = "开始扫描"; "scan_result" = "扫描结果:"; +"language" = "语言"; // 错误信息 "scan_error_title" = "扫描失败";