v504 2 months ago
parent d3c398583b
commit ab7cab87bd

@ -6,6 +6,11 @@
objectVersion = 77; objectVersion = 77;
objects = { 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 */ /* Begin PBXContainerItemProxy section */
5817663D2E54241300C1B687 /* PBXContainerItemProxy */ = { 5817663D2E54241300C1B687 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
@ -27,11 +32,25 @@
5817662F2E54241200C1B687 /* MyQrCode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MyQrCode.app; sourceTree = BUILT_PRODUCTS_DIR; }; 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; }; 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; }; 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 */ /* 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 */ /* Begin PBXFileSystemSynchronizedRootGroup section */
581766312E54241200C1B687 /* MyQrCode */ = { 581766312E54241200C1B687 /* MyQrCode */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
581766792E544B6E00C1B687 /* Exceptions for "MyQrCode" folder in "MyQrCode" target */,
);
path = MyQrCode; path = MyQrCode;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -52,6 +71,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
581766772E544AFF00C1B687 /* AVFoundation.framework in Frameworks */,
5817666F2E54497800C1B687 /* QRCode in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -78,6 +99,7 @@
581766312E54241200C1B687 /* MyQrCode */, 581766312E54241200C1B687 /* MyQrCode */,
5817663F2E54241300C1B687 /* MyQrCodeTests */, 5817663F2E54241300C1B687 /* MyQrCodeTests */,
581766492E54241300C1B687 /* MyQrCodeUITests */, 581766492E54241300C1B687 /* MyQrCodeUITests */,
581766742E544AEC00C1B687 /* Frameworks */,
581766302E54241200C1B687 /* Products */, 581766302E54241200C1B687 /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@ -92,6 +114,14 @@
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
581766742E544AEC00C1B687 /* Frameworks */ = {
isa = PBXGroup;
children = (
581766752E544AEC00C1B687 /* AVFoundation.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -112,6 +142,7 @@
); );
name = MyQrCode; name = MyQrCode;
packageProductDependencies = ( packageProductDependencies = (
5817666E2E54497800C1B687 /* QRCode */,
); );
productName = MyQrCode; productName = MyQrCode;
productReference = 5817662F2E54241200C1B687 /* MyQrCode.app */; productReference = 5817662F2E54241200C1B687 /* MyQrCode.app */;
@ -195,6 +226,9 @@
); );
mainGroup = 581766262E54241200C1B687; mainGroup = 581766262E54241200C1B687;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = (
5817666D2E54497800C1B687 /* XCRemoteSwiftPackageReference "qrcode" */,
);
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = 581766302E54241200C1B687 /* Products */; productRefGroup = 581766302E54241200C1B687 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -400,11 +434,15 @@
DEVELOPMENT_TEAM = 6AS7587HX4; DEVELOPMENT_TEAM = 6AS7587HX4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -432,11 +470,15 @@
DEVELOPMENT_TEAM = 6AS7587HX4; DEVELOPMENT_TEAM = 6AS7587HX4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -578,6 +620,25 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = 581766272E54241200C1B687 /* Project object */;
} }

@ -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
}

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
uuid = "7FD2F711-DEA4-44E1-A4A2-14C028D9E1E6"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "A66FDEBC-88C2-4379-BD86-F86ABD338132"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "MyQrCode/ScannerView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "154"
endingLineNumber = "154"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "BAD1F3E5-40C4-48EC-8689-8EE142990188"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "MyQrCode/ScannerView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "155"
endingLineNumber = "155"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "7F5C90A9-5ED1-458F-8709-80FB15BEDF54"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "MyQrCode/ScannerView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "147"
endingLineNumber = "147"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "D72F93EE-889D-443A-A073-E83F7C9D6409"
shouldBeEnabled = "No"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "MyQrCode/ScannerView.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "175"
endingLineNumber = "175"
landmarkName = "body"
landmarkType = "24">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

@ -6,18 +6,65 @@
// //
import SwiftUI import SwiftUI
import QRCode
struct ContentView: View { struct ContentView: View {
@State private var showScanner = false
@State private var scannedCode: String?
var body: some View { var body: some View {
VStack { NavigationView {
Image(systemName: "globe") VStack(spacing: 30) {
.imageScale(.large) Image(systemName: "qrcode.viewfinder")
.foregroundStyle(.tint) .font(.system(size: 100))
Text("Hello, world!") .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 { #Preview {
ContentView() ContentView()

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>

@ -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()
}

@ -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 中添加相机权限描述
- 扫描器支持屏幕旋转和尺寸变化
- 条码位置标记会自动适应屏幕变化
Loading…
Cancel
Save