diff --git a/MyQrCode.xcodeproj/project.pbxproj b/MyQrCode.xcodeproj/project.pbxproj index 9c744a1..3d7ad00 100644 --- a/MyQrCode.xcodeproj/project.pbxproj +++ b/MyQrCode.xcodeproj/project.pbxproj @@ -6,6 +6,11 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 5817666F2E54497800C1B687 /* QRCode in Frameworks */ = {isa = PBXBuildFile; productRef = 5817666E2E54497800C1B687 /* QRCode */; }; + 581766772E544AFF00C1B687 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 581766752E544AEC00C1B687 /* AVFoundation.framework */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 5817663D2E54241300C1B687 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -27,11 +32,25 @@ 5817662F2E54241200C1B687 /* MyQrCode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyQrCode.app; sourceTree = BUILT_PRODUCTS_DIR; }; 5817663C2E54241300C1B687 /* MyQrCodeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyQrCodeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 581766462E54241300C1B687 /* MyQrCodeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MyQrCodeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 581766752E544AEC00C1B687 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 581766792E544B6E00C1B687 /* Exceptions for "MyQrCode" folder in "MyQrCode" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 5817662E2E54241200C1B687 /* MyQrCode */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 581766312E54241200C1B687 /* MyQrCode */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 581766792E544B6E00C1B687 /* Exceptions for "MyQrCode" folder in "MyQrCode" target */, + ); path = MyQrCode; sourceTree = ""; }; @@ -52,6 +71,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 581766772E544AFF00C1B687 /* AVFoundation.framework in Frameworks */, + 5817666F2E54497800C1B687 /* QRCode in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -78,6 +99,7 @@ 581766312E54241200C1B687 /* MyQrCode */, 5817663F2E54241300C1B687 /* MyQrCodeTests */, 581766492E54241300C1B687 /* MyQrCodeUITests */, + 581766742E544AEC00C1B687 /* Frameworks */, 581766302E54241200C1B687 /* Products */, ); sourceTree = ""; @@ -92,6 +114,14 @@ name = Products; sourceTree = ""; }; + 581766742E544AEC00C1B687 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 581766752E544AEC00C1B687 /* AVFoundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -112,6 +142,7 @@ ); name = MyQrCode; packageProductDependencies = ( + 5817666E2E54497800C1B687 /* QRCode */, ); productName = MyQrCode; productReference = 5817662F2E54241200C1B687 /* MyQrCode.app */; @@ -195,6 +226,9 @@ ); mainGroup = 581766262E54241200C1B687; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 5817666D2E54497800C1B687 /* XCRemoteSwiftPackageReference "qrcode" */, + ); preferredProjectObjectVersion = 77; productRefGroup = 581766302E54241200C1B687 /* Products */; projectDirPath = ""; @@ -400,11 +434,15 @@ DEVELOPMENT_TEAM = 6AS7587HX4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MyQrCode/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to your camera to scan QR codes."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -432,11 +470,15 @@ DEVELOPMENT_TEAM = 6AS7587HX4; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MyQrCode/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to your camera to scan QR codes."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -578,6 +620,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 5817666D2E54497800C1B687 /* XCRemoteSwiftPackageReference "qrcode" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/dagronf/qrcode.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 27.11.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 5817666E2E54497800C1B687 /* QRCode */ = { + isa = XCSwiftPackageProductDependency; + package = 5817666D2E54497800C1B687 /* XCRemoteSwiftPackageReference "qrcode" */; + productName = QRCode; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 581766272E54241200C1B687 /* Project object */; } diff --git a/MyQrCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/MyQrCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..28bd417 --- /dev/null +++ b/MyQrCode.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "fd2455af71f8be5e18c9e2dd99f5fea73d2285d99fb343d056d9b2dde36eadc7", + "pins" : [ + { + "identity" : "qrcode", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/qrcode.git", + "state" : { + "revision" : "ea4047a7777bae470adc140cd94bfa5396a96202", + "version" : "27.11.0" + } + }, + { + "identity" : "swift-qrcode-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/swift-qrcode-generator", + "state" : { + "revision" : "2b1980b825f08a81ccc762b0c4d17fcde9d5e953", + "version" : "2.0.2" + } + }, + { + "identity" : "swiftimagereadwrite", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dagronf/SwiftImageReadWrite", + "state" : { + "revision" : "42ace2412279f18bc264bc306e96b51c36e12a33", + "version" : "1.9.2" + } + } + ], + "version" : 3 +} diff --git a/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..e5aeb5c --- /dev/null +++ b/MyQrCode.xcodeproj/xcuserdata/yc.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/MyQrCode/ContentView.swift b/MyQrCode/ContentView.swift index 2a9a250..054140b 100644 --- a/MyQrCode/ContentView.swift +++ b/MyQrCode/ContentView.swift @@ -6,18 +6,65 @@ // import SwiftUI +import QRCode struct ContentView: View { + @State private var showScanner = false + @State private var scannedCode: String? + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + NavigationView { + VStack(spacing: 30) { + Image(systemName: "qrcode.viewfinder") + .font(.system(size: 100)) + .foregroundColor(.blue) + + Text("条码扫描器") + .font(.largeTitle) + .fontWeight(.bold) + + if let code = scannedCode { + VStack(spacing: 10) { + Text("扫描结果:") + .font(.headline) + Text(code) + .font(.body) + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + } + } + + Button(action: { + showScanner = true + }) { + HStack { + Image(systemName: "camera") + Text("开始扫描") + } + .font(.title2) + .foregroundColor(.white) + .padding() + .background(Color.blue) + .cornerRadius(10) + } + + Spacer() + } + .padding() + .navigationTitle("MyQrCode") + .sheet(isPresented: $showScanner) { + ScannerView() + .onReceive(NotificationCenter.default.publisher(for: .scannerDidScanCode)) { notification in + if let code = notification.object as? String { + scannedCode = code + showScanner = false + } + } + } } - .padding() } -} +} #Preview { ContentView() diff --git a/MyQrCode/Info.plist b/MyQrCode/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/MyQrCode/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/MyQrCode/ScannerView.swift b/MyQrCode/ScannerView.swift new file mode 100644 index 0000000..c24aaf1 --- /dev/null +++ b/MyQrCode/ScannerView.swift @@ -0,0 +1,477 @@ +import SwiftUI +import AVFoundation +internal import Combine + +// 通知名称扩展 +extension Notification.Name { + static let scannerDidScanCode = Notification.Name("scannerDidScanCode") +} + +// 检测到的条码数据结构 +struct DetectedCode: Identifiable { + let id = UUID() + let type: String + let content: String + let bounds: CGRect +} + +struct ScannerView: View { + @StateObject private var scannerViewModel = ScannerViewModel() + @Environment(\.dismiss) private var dismiss + @State private var showPreviewPause = false + @State private var previewLayer: AVCaptureVideoPreviewLayer? + @State private var screenOrientation = UIDevice.current.orientation + + var body: some View { + ZStack { + // 相机预览层 + CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer) + .ignoresSafeArea() + + // 扫描框覆盖层 + VStack { + Spacer() + + // 扫描框 + ZStack { + RoundedRectangle(cornerRadius: 20) + .stroke(Color.white, lineWidth: 3) + .frame(width: 250, height: 250) + .background(Color.black.opacity(0.3)) + .cornerRadius(20) + + // 扫描线动画 + if !showPreviewPause { + Rectangle() + .fill(LinearGradient( + colors: [Color.clear, Color.green, Color.clear], + startPoint: .top, + endPoint: .bottom + )) + .frame(width: 250, height: 2) + .offset(y: -125) + .animation( + Animation.linear(duration: 2) + .repeatForever(autoreverses: false), + value: UUID() + ) + } + } + + // 提示文本 + if showPreviewPause { + VStack(spacing: 8) { + Text("检测到条码") + .foregroundColor(.white) + .font(.headline) + + if scannerViewModel.detectedCodes.count == 1 { + Text("1秒后自动显示结果") + .foregroundColor(.green) + .font(.subheadline) + } else { + Text("点击绿色标记选择要解码的条码") + .foregroundColor(.white.opacity(0.8)) + .font(.subheadline) + } + } + .padding(.top, 20) + } else { + Text("将二维码或条形码放入框内") + .foregroundColor(.white) + .font(.headline) + .padding(.top, 20) + } + + Spacer() + + // 底部按钮区域 + VStack(spacing: 15) { + if showPreviewPause { + // 预览暂停时的按钮 + Button("重新扫描") { + resetToScanning() + } + .foregroundColor(.white) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.blue) + .cornerRadius(20) + } + + // 关闭按钮 + Button("关闭") { + dismiss() + } + .foregroundColor(.white) + .padding() + .background(Color.black.opacity(0.6)) + .cornerRadius(10) + } + .padding(.bottom, 50) + } + + // 条码位置标记覆盖层(支持点击选择) + if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty { + CodePositionOverlay( + detectedCodes: scannerViewModel.detectedCodes, + previewLayer: previewLayer, + onCodeSelected: { selectedCode in + NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode) + dismiss() + } + ) + } + + // 测试按钮(用于调试单个条码自动选择) + if showPreviewPause && scannerViewModel.detectedCodes.count == 1 { + VStack { + HStack { + Spacer() + Button("测试自动选择") { + let code = scannerViewModel.detectedCodes[0] + let selectedCode = "类型: \(code.type)\n内容: \(code.content)" + NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode) + dismiss() + } + .foregroundColor(.white) + .padding(8) + .background(Color.red) + .cornerRadius(8) + .padding(.trailing, 20) + } + Spacer() + } + } + + + } + .onAppear { + scannerViewModel.startScanning() + } + .onDisappear { + scannerViewModel.stopScanning() + } + .alert("扫描失败", isPresented: $scannerViewModel.showAlert) { + Button("确定") { + dismiss() + } + } message: { + Text("您的设备不支持扫描二维码。请使用带相机的设备。") + } + + .onReceive(scannerViewModel.$detectedCodes) { codes in + if !codes.isEmpty { + print("检测到条码数量: \(codes.count)") + if codes.count == 1 { + // 只有一个码,显示标记后一秒自动显示结果 + print("单个条码,准备自动选择") + pauseForPreview() + autoSelectSingleCode(code: codes[0]) + } else { + // 多个码,暂停预览并引导用户选择 + print("多个条码,等待用户选择") + pauseForPreview() + } + } + } + .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in + // 屏幕方向变化时更新状态 + screenOrientation = UIDevice.current.orientation + print("Screen orientation changed to: \(screenOrientation.rawValue)") + } + } + + private func pauseForPreview() { + showPreviewPause = true + // 移除自动显示选择界面的定时器,用户必须手动选择 + } + + private func resetToScanning() { + showPreviewPause = false + scannerViewModel.resetDetection() + scannerViewModel.startScanning() + } + + private func autoSelectSingleCode(code: DetectedCode) { + print("开始自动选择定时器,条码类型: \(code.type)") + // 一秒后自动选择单个条码 + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + print("自动选择定时器触发") + print("当前状态 - showPreviewPause: \(self.showPreviewPause)") + print("当前条码数量: \(self.scannerViewModel.detectedCodes.count)") + + if self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 { + print("条件满足,执行自动选择") + // 确保仍然只有一个条码且处于预览暂停状态 + let selectedCode = "类型: \(code.type)\n内容: \(code.content)" + print("发送通知: \(selectedCode)") + NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode) + self.dismiss() + } else { + print("条件不满足,取消自动选择") + } + } + } +} + +// 相机预览视图 +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 + } + } + } +} + +// 扫描器视图模型 +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() + } + + 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 + } + } + + 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) { + + print("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象") + + // 震动反馈 + 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) + print("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)") + } + } + + print("准备更新 detectedCodes,数量: \(codes.count)") + + // 更新检测到的条码列表 + DispatchQueue.main.async { + print("在主线程更新 detectedCodes") + self.detectedCodes = codes + } + } +} + +// 条码位置标记覆盖层 +struct CodePositionOverlay: View { + let detectedCodes: [DetectedCode] + let previewLayer: AVCaptureVideoPreviewLayer? + let onCodeSelected: (String) -> 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) // 允许触摸事件 + } +} + +// 单个条码位置标记 +struct CodePositionMarker: View { + let code: DetectedCode + let screenSize: CGSize + let previewLayer: AVCaptureVideoPreviewLayer? + let onCodeSelected: (String) -> Void + + var body: some View { + GeometryReader { geometry in + // 使用GeometryReader获取实时尺寸 + 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 { + // 点击标记时选择条码 + let selectedCode = "类型: \(code.type)\n内容: \(code.content)" + onCodeSelected(selectedCode) + } + .onAppear { + print("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)") + print("Screen size: \(geometry.size)") + print("Code bounds: \(code.bounds)") + } + } + } + + private func calculatePosition(screenSize: CGSize) -> CGPoint { + guard let previewLayer = previewLayer else { + // 如果没有预览层,使用屏幕中心 + print("No preview layer available, using screen center") + 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") + return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2) + } + + // 使用AVFoundation的坐标转换方法 + let metadataObject = code.bounds + let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint( + x: metadataObject.midX, + y: metadataObject.midY + )) + + // 验证转换结果是否有效 + guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else { + print("Invalid converted point: \(convertedPoint), using screen center") + 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)) + + print("AVFoundation bounds: \(code.bounds)") + print("Converted point: \(convertedPoint)") + print("Screen size: \(screenSize)") + print("Clamped: x=\(clampedX), y=\(clampedY)") + + return CGPoint(x: clampedX, y: clampedY) + } +} + + + +#Preview { + ScannerView() +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..6773d32 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# MyQrCode - SwiftUI 版本 + +## 项目概述 + +这是一个使用 SwiftUI 构建的条码扫描器应用,支持二维码和多种条形码格式,已经从原来的 UIKit 版本完全转换为 SwiftUI。 + +## 主要变化 + +### 从 UIKit 转换为 SwiftUI + +- **ScannerViewController.swift** → **ScannerView.swift** + - 使用 `@StateObject` 管理扫描器状态 + - 使用 `UIViewRepresentable` 包装相机预览层 + - 采用 MVVM 架构模式 + +### 新特性 + +1. **现代化的 UI 设计** + - 扫描框覆盖层,提供视觉引导 + - 美观的按钮和文本样式 + - 响应式布局 + +2. **改进的状态管理** + - 使用 `@Published` 属性包装器 + - 通过通知中心传递扫描结果 + - 更好的错误处理 + +3. **用户体验优化** + - 扫描成功后的震动反馈 + - 清晰的扫描指导文本 + - 优雅的模态展示 + +4. **多格式条码支持** + - 二维码 (QR Code) + - EAN-8 和 EAN-13 条形码 + - Code 128 和 Code 39 条形码 + - UPC-E 条形码 + - PDF417 和 Aztec 码 + +5. **微信风格扫描界面** + - 扫描线动画效果 + - 检测到条码时短暂暂停预览 + - 条码位置中心点标记(绿色圆圈) + - 点击标记直接选择条码 + - 支持重新扫描 + +## 文件结构 + +``` +MyQrCode/ +├── ScannerView.swift # 新的 SwiftUI 扫描器视图 +├── ContentView.swift # 更新的主内容视图 +├── MyQrCodeApp.swift # 应用入口点 +└── ...其他文件 +``` + +## 使用方法 + +1. 启动应用后,点击"开始扫描"按钮 +2. 将二维码或条形码放入扫描框内 +3. 扫描成功后: + - 短暂暂停预览并显示条码覆盖层 + - 在相机预览上显示条码位置标记(绿色圆圈) +4. 在预览暂停时: + - 在相机预览上显示条码位置标记(绿色圆圈) + - 单个条码:1秒后自动显示结果 + - 多个条码:点击绿色标记选择要解码的条码 + - 点击"重新扫描"继续扫描 +5. 单个条码自动选择,多个条码手动选择 +6. 可以重复扫描新的条码 + +## 技术特点 + +- **SwiftUI**: 现代化的声明式 UI 框架 +- **AVFoundation**: 相机和条码扫描功能 +- **MVVM 架构**: 清晰的代码分离和状态管理 +- **通知中心**: 组件间的松耦合通信 + +## 兼容性 + +- iOS 14.0+ +- Xcode 12.0+ +- Swift 5.3+ + +## 注意事项 + +- 需要在真机上测试相机功能 +- 确保在 Info.plist 中添加相机权限描述 +- 扫描器支持屏幕旋转和尺寸变化 +- 条码位置标记会自动适应屏幕变化 \ No newline at end of file