diff --git a/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 1a5eb99..e13d109 100644 --- a/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -16,8 +16,8 @@ endingColumnNumber = "9223372036854775807" startingLineNumber = "146" endingLineNumber = "146" - landmarkName = "ScanningOverlayView" - landmarkType = "14"> + landmarkName = "resetToScanning()" + landmarkType = "7"> + landmarkName = "resetToScanning()" + landmarkType = "7"> + landmarkName = "resetToScanning()" + landmarkType = "7"> + landmarkName = "ScanningOverlayView" + landmarkType = "14"> diff --git a/MyQrCode/ScannerView.swift b/MyQrCode/ScannerView.swift index d20f8d2..5e07cad 100644 --- a/MyQrCode/ScannerView.swift +++ b/MyQrCode/ScannerView.swift @@ -15,38 +15,55 @@ struct ScannerView: View { var body: some View { ZStack { - // 相机预览层 - CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) - .ignoresSafeArea() - - // 扫描界面覆盖层 - ScanningOverlayView( - showPreviewPause: showPreviewPause, - selectedStyle: $selectedScanningStyle, - detectedCodesCount: scannerViewModel.detectedCodes.count, - onClose: { dismiss() } - ) - - // 条码位置标记覆盖层 - if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { - CodePositionOverlay( - detectedCodes: scannerViewModel.detectedCodes, - previewLayer: previewLayer, - onCodeSelected: handleCodeSelection, - onRescan: resetToScanning + // 相机权限检查 + if scannerViewModel.cameraAuthorizationStatus == .authorized { + // 相机预览层 + CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) + .ignoresSafeArea() + + // 扫描界面覆盖层 + ScanningOverlayView( + showPreviewPause: showPreviewPause, + selectedStyle: $selectedScanningStyle, + detectedCodesCount: scannerViewModel.detectedCodes.count, + onClose: { dismiss() } ) - } - - // 测试按钮(调试用) - if showPreviewPause && scannerViewModel.detectedCodes.count == 1 { - TestAutoSelectButton( - detectedCode: scannerViewModel.detectedCodes[0], - onSelect: handleCodeSelection + + // 条码位置标记覆盖层 + if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { + CodePositionOverlay( + detectedCodes: scannerViewModel.detectedCodes, + previewLayer: previewLayer, + onCodeSelected: handleCodeSelection, + onRescan: resetToScanning + ) + } + + // 测试按钮(调试用) + if showPreviewPause && scannerViewModel.detectedCodes.count == 1 { + TestAutoSelectButton( + detectedCode: scannerViewModel.detectedCodes[0], + onSelect: handleCodeSelection + ) + } + } else { + // 权限未授权时的UI + CameraPermissionView( + authorizationStatus: scannerViewModel.cameraAuthorizationStatus, + onRequestPermission: { + scannerViewModel.refreshCameraPermission() + }, + onOpenSettings: { + scannerViewModel.openSettings() + } ) } } .onAppear { - scannerViewModel.startScanning() + // 只有在相机权限已授权时才启动扫描 + if scannerViewModel.cameraAuthorizationStatus == .authorized { + scannerViewModel.startScanning() + } } .onDisappear { scannerViewModel.stopScanning() @@ -59,6 +76,12 @@ struct ScannerView: View { .onReceive(scannerViewModel.$detectedCodes) { codes in handleDetectedCodes(codes) } + .onReceive(scannerViewModel.$cameraAuthorizationStatus) { status in + if status == .authorized { + logInfo("🎯 相机权限已授权,启动扫描", className: "ScannerView") + scannerViewModel.startScanning() + } + } .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in handleOrientationChange() } @@ -311,13 +334,78 @@ struct CameraPreviewView: UIViewRepresentable { class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate { @Published var detectedCodes: [DetectedCode] = [] @Published var showAlert = false + @Published var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined + @Published var showPermissionAlert = false var captureSession: AVCaptureSession! private var metadataOutput: AVCaptureMetadataOutput? override init() { super.init() - setupCaptureSession() + 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: - 相机设置 @@ -882,6 +970,85 @@ extension Notification.Name { static let scannerDidScanCode = Notification.Name("scannerDidScanCode") } +// MARK: - 相机权限视图 +struct CameraPermissionView: View { + let authorizationStatus: AVAuthorizationStatus + let onRequestPermission: () -> Void + let onOpenSettings: () -> Void + + var body: some View { + VStack(spacing: 30) { + Spacer() + + // 相机图标 + Image(systemName: "camera.fill") + .font(.system(size: 80)) + .foregroundColor(.gray) + + // 标题 + Text("camera_permission_title".localized) + .font(.largeTitle) + .fontWeight(.bold) + .multilineTextAlignment(.center) + + // 描述文本 + Text(getDescriptionText()) + .font(.body) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + .padding(.horizontal, 40) + + // 操作按钮 + VStack(spacing: 15) { + if authorizationStatus == .notDetermined { + Button(action: onRequestPermission) { + HStack { + Image(systemName: "camera.badge.ellipsis") + Text("request_camera_permission".localized) + } + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue) + .cornerRadius(12) + } + } else if authorizationStatus == .denied || authorizationStatus == .restricted { + Button(action: onOpenSettings) { + HStack { + Image(systemName: "gear") + Text("open_settings".localized) + } + .font(.headline) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .padding() + .background(Color.orange) + .cornerRadius(12) + } + } + } + .padding(.horizontal, 40) + + Spacer() + } + .background(Color(.systemBackground)) + } + + private func getDescriptionText() -> String { + switch authorizationStatus { + case .notDetermined: + return "camera_permission_description".localized + case .denied: + return "camera_permission_denied".localized + case .restricted: + return "camera_permission_restricted".localized + default: + return "camera_permission_unknown".localized + } + } +} + #if DEBUG struct ScannerView_Previews: PreviewProvider { static var previews: some View { diff --git a/MyQrCode/en.lproj/Localizable.strings b/MyQrCode/en.lproj/Localizable.strings index d7f1cfe..34b4c41 100644 --- a/MyQrCode/en.lproj/Localizable.strings +++ b/MyQrCode/en.lproj/Localizable.strings @@ -34,6 +34,15 @@ // Test Button "test_auto_select" = "Test Auto Select"; +// Camera Permission +"camera_permission_title" = "Camera Permission Required"; +"camera_permission_description" = "This app needs access to your camera to scan QR codes and barcodes. Please grant camera permission to continue."; +"camera_permission_denied" = "Camera access has been denied. Please enable camera permission in Settings to use the scanner."; +"camera_permission_restricted" = "Camera access is restricted. Please check your device settings or contact your administrator."; +"camera_permission_unknown" = "Camera permission status is unknown. Please check your device settings."; +"request_camera_permission" = "Grant Camera Access"; +"open_settings" = "Open Settings"; + // Language Settings "select_language" = "Select Language"; "language_changes_info" = "Language changes will take effect immediately"; diff --git a/MyQrCode/zh-Hans.lproj/Localizable.strings b/MyQrCode/zh-Hans.lproj/Localizable.strings index 9b72c84..07f259e 100644 --- a/MyQrCode/zh-Hans.lproj/Localizable.strings +++ b/MyQrCode/zh-Hans.lproj/Localizable.strings @@ -34,6 +34,15 @@ // 测试按钮 "test_auto_select" = "测试自动选择"; +// 相机权限 +"camera_permission_title" = "需要相机权限"; +"camera_permission_description" = "此应用需要访问您的相机来扫描二维码和条形码。请授予相机权限以继续使用。"; +"camera_permission_denied" = "相机访问被拒绝。请在设置中启用相机权限以使用扫描器。"; +"camera_permission_restricted" = "相机访问受限。请检查您的设备设置或联系管理员。"; +"camera_permission_unknown" = "相机权限状态未知。请检查您的设备设置。"; +"request_camera_permission" = "授予相机权限"; +"open_settings" = "打开设置"; + // 语言设置 "select_language" = "选择语言"; "language_changes_info" = "语言更改将立即生效";