import SwiftUI import AVFoundation import AudioToolbox import Combine // MARK: - 主扫描视图 struct ScannerView: View { @StateObject private var scannerViewModel = ScannerViewModel() @State private var showPreviewPause = false @State private var selectedScanningStyle: ScanningLineStyle = .modern @State private var screenOrientation = UIDevice.current.orientation @State private var previewLayer: AVCaptureVideoPreviewLayer? @Environment(\.dismiss) private var dismiss var body: some View { ZStack { // 相机预览层 CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) .ignoresSafeArea() // 扫描界面覆盖层 ScanningOverlayView( showPreviewPause: showPreviewPause, selectedStyle: $selectedScanningStyle, detectedCodesCount: scannerViewModel.detectedCodes.count, onRescan: resetToScanning, onClose: { dismiss() } ) // 条码位置标记覆盖层 if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { CodePositionOverlay( detectedCodes: scannerViewModel.detectedCodes, previewLayer: previewLayer, onCodeSelected: handleCodeSelection ) } // 测试按钮(调试用) if showPreviewPause && scannerViewModel.detectedCodes.count == 1 { TestAutoSelectButton( detectedCode: scannerViewModel.detectedCodes[0], onSelect: handleCodeSelection ) } } .onAppear { scannerViewModel.startScanning() } .onDisappear { scannerViewModel.stopScanning() } .alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) { Button("OK") { } } message: { Text("scan_error_message".localized) } .onReceive(scannerViewModel.$detectedCodes) { codes in handleDetectedCodes(codes) } .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in handleOrientationChange() } } // MARK: - 私有方法 private func handleDetectedCodes(_ codes: [DetectedCode]) { guard !codes.isEmpty else { return } logInfo("检测到条码数量: \(codes.count)", className: "ScannerView") if codes.count == 1 { logInfo("单个条码,准备自动选择", className: "ScannerView") pauseForPreview() autoSelectSingleCode(code: codes[0]) } else { logInfo("多个条码,等待用户选择", className: "ScannerView") pauseForPreview() } } private func handleOrientationChange() { screenOrientation = UIDevice.current.orientation logInfo("Screen orientation changed to: \(screenOrientation.rawValue)", className: "ScannerView") } private func handleCodeSelection(_ selectedCode: DetectedCode) { logInfo("用户选择了条码: \(selectedCode.content)", className: "ScannerView") let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)" NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult) dismiss() } private func pauseForPreview() { showPreviewPause = true } private func resetToScanning() { showPreviewPause = false scannerViewModel.resetDetection() scannerViewModel.startScanning() } private func autoSelectSingleCode(code: DetectedCode) { logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView") DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { guard self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 else { logInfo("条件不满足,取消自动选择", className: "ScannerView") return } logInfo("条件满足,执行自动选择", className: "ScannerView") self.handleCodeSelection(code) } } } // MARK: - 扫描界面覆盖层 struct ScanningOverlayView: View { let showPreviewPause: Bool @Binding var selectedStyle: ScanningLineStyle let detectedCodesCount: Int let onRescan: () -> Void let onClose: () -> Void var body: some View { VStack { Spacer() // 扫描线组件 if !showPreviewPause { ScanningLineView(style: selectedStyle) } // 提示文本 ScanningInstructionView( showPreviewPause: showPreviewPause, detectedCodesCount: detectedCodesCount ) Spacer() // 底部按钮区域 ScanningBottomButtonsView( showPreviewPause: showPreviewPause, selectedStyle: $selectedStyle, onRescan: onRescan, onClose: onClose ) } } } // MARK: - 扫描指令视图 struct ScanningInstructionView: View { let showPreviewPause: Bool let detectedCodesCount: Int var body: some View { if showPreviewPause { VStack(spacing: 8) { Text("detected_codes".localized) .foregroundColor(.white) .font(.headline) if detectedCodesCount == 1 { Text("auto_result_1s".localized) .foregroundColor(.green) .font(.subheadline) } else { Text("select_code_instruction".localized) .foregroundColor(.white.opacity(0.8)) .font(.subheadline) } } .padding(.top, 20) } else { Text("scan_instruction".localized) .foregroundColor(.white) .font(.headline) .padding(.top, 20) } } } // MARK: - 扫描底部按钮视图 struct ScanningBottomButtonsView: View { let showPreviewPause: Bool @Binding var selectedStyle: ScanningLineStyle let onRescan: () -> Void let onClose: () -> Void var body: some View { VStack(spacing: 15) { // 扫描线样式选择器 if !showPreviewPause { ScanningStyleSelectorView(selectedStyle: $selectedStyle) } if showPreviewPause { // 预览暂停时的按钮 Button("rescan_button".localized) { onRescan() } .foregroundColor(.white) .padding(.horizontal, 20) .padding(.vertical, 10) .background(Color.blue) .cornerRadius(20) } // 关闭按钮 Button("close_button".localized) { onClose() } .foregroundColor(.white) .padding() .background(Color.black.opacity(0.6)) .cornerRadius(10) } .padding(.bottom, 50) } } // MARK: - 扫描线样式选择器 struct ScanningStyleSelectorView: View { @Binding var selectedStyle: ScanningLineStyle var body: some View { HStack(spacing: 10) { ForEach(ScanningLineStyle.allCases, id: \.self) { style in Button(style.localizedName) { selectedStyle = style } .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 4) .background(selectedStyle == style ? Color.green : Color.gray.opacity(0.6)) .cornerRadius(8) .font(.caption) } } .padding(.bottom, 10) } } // MARK: - 测试自动选择按钮 struct TestAutoSelectButton: View { let detectedCode: DetectedCode let onSelect: (DetectedCode) -> Void var body: some View { VStack { HStack { Spacer() Button("test_auto_select".localized) { onSelect(detectedCode) } .foregroundColor(.white) .padding(8) .background(Color.red) .cornerRadius(8) .padding(.trailing, 20) } Spacer() } } } // MARK: - 相机预览视图 struct CameraPreviewView: UIViewRepresentable { let session: AVCaptureSession @Binding var previewLayer: AVCaptureVideoPreviewLayer? func makeUIView(context: Context) -> UIView { let view = UIView() let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.frame = view.bounds previewLayer.videoGravity = .resizeAspectFill view.layer.addSublayer(previewLayer) DispatchQueue.main.async { self.previewLayer = previewLayer } return view } func updateUIView(_ uiView: UIView, context: Context) { if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer { previewLayer.frame = uiView.bounds DispatchQueue.main.async { self.previewLayer = previewLayer } } } } // MARK: - 扫描器视图模型 class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate { @Published var detectedCodes: [DetectedCode] = [] @Published var showAlert = false var captureSession: AVCaptureSession! private var metadataOutput: AVCaptureMetadataOutput? override init() { super.init() setupCaptureSession() } // MARK: - 相机设置 private func setupCaptureSession() { captureSession = AVCaptureSession() guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { showAlert = true return } 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() { DispatchQueue.global(qos: .background).async { [weak self] in self?.captureSession?.startRunning() } } func stopScanning() { DispatchQueue.global(qos: .background).async { [weak self] in self?.captureSession?.stopRunning() } } func resetDetection() { DispatchQueue.main.async { self.detectedCodes = [] } } // MARK: - AVCaptureMetadataOutputObjectsDelegate func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel") // 震动反馈 AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) // 停止扫描 stopScanning() // 处理所有检测到的条码 var codes: [DetectedCode] = [] for metadataObject in metadataObjects { if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, let stringValue = readableObject.stringValue { let codeType = readableObject.type.rawValue let bounds = readableObject.bounds let detectedCode = DetectedCode( type: codeType, content: stringValue, bounds: bounds ) 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: - 条码位置标记覆盖层 struct CodePositionOverlay: View { let detectedCodes: [DetectedCode] let previewLayer: AVCaptureVideoPreviewLayer? let onCodeSelected: (DetectedCode) -> Void var body: some View { GeometryReader { geometry in ZStack { ForEach(detectedCodes) { code in CodePositionMarker( code: code, screenSize: geometry.size, previewLayer: previewLayer, onCodeSelected: onCodeSelected ) } } } .allowsHitTesting(true) } } // MARK: - 单个条码位置标记 struct CodePositionMarker: View { let code: DetectedCode let screenSize: CGSize let previewLayer: AVCaptureVideoPreviewLayer? let onCodeSelected: (DetectedCode) -> Void var body: some View { GeometryReader { geometry in let position = calculatePosition(screenSize: geometry.size) ZStack { // 外圈 Circle() .stroke(Color.green, lineWidth: 3) .frame(width: 40, height: 40) // 内圈 Circle() .fill(Color.green.opacity(0.3)) .frame(width: 20, height: 20) // 中心点 Circle() .fill(Color.green) .frame(width: 6, height: 6) } .position(x: position.x, y: position.y) .background( Circle() .fill(Color.clear) .frame(width: 60, height: 60) ) .onTapGesture { onCodeSelected(code) } .onAppear { 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") } } } private func calculatePosition(screenSize: CGSize) -> CGPoint { guard let previewLayer = previewLayer else { 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 { logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker") return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } let metadataObject = code.bounds let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint( x: metadataObject.midX, y: metadataObject.midY )) guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else { logWarning("Invalid converted point: \(convertedPoint), using screen center", className: "CodePositionMarker") return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) } let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x)) let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y)) 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) } } // MARK: - 扫描线动画修饰符 struct ScanningLineModifier: ViewModifier { @State private var isAnimating = false func body(content: Content) -> some View { content .offset(y: isAnimating ? 150 : -150) .onAppear { withAnimation( Animation.linear(duration: 2) .repeatForever(autoreverses: false) ) { isAnimating = true } } } } // MARK: - 脉冲动画修饰符 struct PulseAnimationModifier: ViewModifier { @State private var isPulsing = false func body(content: Content) -> some View { content .scaleEffect(isPulsing ? 1.5 : 1.0) .opacity(isPulsing ? 0.0 : 0.8) .onAppear { withAnimation( Animation.easeInOut(duration: 1.5) .repeatForever(autoreverses: false) ) { isPulsing = true } } } } // MARK: - 扫描线样式枚举 enum ScanningLineStyle: String, CaseIterable { case modern = "style_modern" case classic = "style_classic" case neon = "style_neon" case minimal = "style_minimal" case retro = "style_retro" var localizedName: String { switch self { case .modern: return "style_modern".localized case .classic: return "style_classic".localized case .neon: return "style_neon".localized case .minimal: return "style_minimal".localized case .retro: return "style_retro".localized } } } // MARK: - 扫描线视图 struct ScanningLineView: View { let style: ScanningLineStyle var body: some View { Group { switch style { case .modern: ModernScanningLine() case .classic: ClassicScanningLine() case .neon: NeonScanningLine() case .minimal: MinimalScanningLine() case .retro: RetroScanningLine() } } } } // MARK: - 现代扫描线 struct ModernScanningLine: View { var body: some View { Rectangle() .fill( LinearGradient( colors: [.blue, .cyan, .blue], startPoint: .leading, endPoint: .trailing ) ) .frame(width: 200, height: 3) .shadow(color: .blue, radius: 5, x: 0, y: 0) .modifier(ScanningLineModifier()) } } // MARK: - 经典扫描线 struct ClassicScanningLine: View { var body: some View { Rectangle() .fill(Color.green) .frame(width: 150, height: 2) .modifier(ScanningLineModifier()) } } // MARK: - 霓虹扫描线 struct NeonScanningLine: View { var body: some View { Rectangle() .fill(Color.purple) .frame(width: 180, height: 4) .shadow(color: .purple, radius: 8, x: 0, y: 0) .modifier(ScanningLineModifier()) } } // MARK: - 极简扫描线 struct MinimalScanningLine: View { var body: some View { Rectangle() .fill(Color.white) .frame(width: 100, height: 1) .modifier(ScanningLineModifier()) } } // MARK: - 复古扫描线 struct RetroScanningLine: View { var body: some View { HStack(spacing: 2) { ForEach(0..<5, id: \.self) { _ in Rectangle() .fill(Color.orange) .frame(width: 2, height: 20) } } .modifier(ScanningLineModifier()) } } // MARK: - 检测到的条码数据结构 struct DetectedCode: Identifiable { let id = UUID() let type: String let content: String let bounds: CGRect } // MARK: - 通知名称扩展 extension Notification.Name { static let scannerDidScanCode = Notification.Name("scannerDidScanCode") } #if DEBUG struct ScannerView_Previews: PreviewProvider { static var previews: some View { ScannerView() } } #endif