Remove obsolete files related to localization and QR code functionality, including various views and utility classes. This cleanup enhances project maintainability by eliminating unused code and streamlining the overall structure.

main
v504 2 months ago
parent 51d83b4f96
commit f3344d6253

@ -171,7 +171,7 @@ class LanguageManager: ObservableObject {
} else {
// 使
currentLanguage = .system
let systemLanguage = detectSystemLanguage()
_ = detectSystemLanguage()
//
UserDefaults.standard.set("system", forKey: languageKey)

@ -1,86 +0,0 @@
import SwiftUI
import AVFoundation
// MARK: -
struct CameraPermissionView: View {
@EnvironmentObject var languageManager: LanguageManager
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)
.id(languageManager.refreshTrigger)
//
Text(getDescriptionText())
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.padding(.horizontal, 40)
.id(languageManager.refreshTrigger)
//
VStack(spacing: 15) {
if authorizationStatus == .notDetermined {
Button(action: onRequestPermission) {
HStack {
Image(systemName: "camera.badge.ellipsis")
Text("request_camera_permission".localized)
.id(languageManager.refreshTrigger)
}
.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)
.id(languageManager.refreshTrigger)
}
.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
}
}
}

@ -1,34 +0,0 @@
import SwiftUI
import AVFoundation
// MARK: -
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
@Binding var previewLayer: AVCaptureVideoPreviewLayer?
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .black
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
//
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
guard let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer else { return }
//
DispatchQueue.main.async {
previewLayer.frame = uiView.bounds
self.previewLayer = previewLayer
}
}
}

@ -1,163 +0,0 @@
import SwiftUI
import AVFoundation
// MARK: -
struct CodePositionOverlay: View {
let detectedCodes: [DetectedCode]
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (DetectedCode) -> Void
var body: some View {
ZStack {
GeometryReader { geometry in
ForEach(detectedCodes) { code in
CodePositionMarker(
code: code,
screenSize: geometry.size,
previewLayer: previewLayer,
onCodeSelected: onCodeSelected
)
}
//
#if DEBUG
ForEach(detectedCodes) { code in
let position = calculateDebugPosition(code: code, screenSize: geometry.size)
Rectangle()
.stroke(Color.red, lineWidth: 1)
.frame(width: 80, height: 80)
.position(x: position.x, y: position.y)
.opacity(0.3)
}
#endif
}
.ignoresSafeArea()
}
.allowsHitTesting(true)
.contentShape(Rectangle()) //
.zIndex(1000) //
}
//
private func calculateDebugPosition(code: DetectedCode, screenSize: CGSize) -> CGPoint {
guard let previewLayer = previewLayer else {
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
))
let clampedX = max(40, min(screenSize.width - 40, convertedPoint.x))
let clampedY = max(40, min(screenSize.height - 40, convertedPoint.y))
return CGPoint(x: clampedX, y: clampedY)
}
}
// 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()
.fill(Color.clear)
.frame(width: 80, height: 80)
.contentShape(Circle())
.onTapGesture {
logDebug("🎯 CodePositionMarker 被点击!", className: "CodePositionMarker")
logDebug(" 条码ID: \(code.id)", className: "CodePositionMarker")
logDebug(" 条码类型: \(code.type)", className: "CodePositionMarker")
logDebug(" 条码内容: \(code.content)", className: "CodePositionMarker")
logDebug(" 点击位置: x=\(position.x), y=\(position.y)", className: "CodePositionMarker")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
onCodeSelected(code)
}
//
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)
.zIndex(1001) //
.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)
}
// 使使
//
let metadataObject = code.bounds
//
guard metadataObject.width > 0 && metadataObject.height > 0 else {
logWarning("Invalid metadata bounds: \(metadataObject), using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
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 RescanButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.opacity(configuration.isPressed ? 0.8 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}

@ -1,22 +0,0 @@
import Foundation
import CoreGraphics
// MARK: -
struct DetectedCode: Identifiable {
let id = UUID()
let type: String
let content: String
let bounds: CGRect
let source: CodeSource //
}
// MARK: -
enum CodeSource {
case camera //
case image //
}
// MARK: -
extension Notification.Name {
static let scannerDidScanCode = Notification.Name("scannerDidScanCode")
}

@ -1,611 +0,0 @@
import SwiftUI
import AVFoundation
import Combine
import CoreData
import QRCode
import Vision
// MARK: -
struct ScannerView: View {
@StateObject private var scannerViewModel = ScannerViewModel()
@EnvironmentObject var languageManager: LanguageManager
@State private var showPreviewPause = false
@State private var previewLayer: AVCaptureVideoPreviewLayer?
@State private var navigateToDetail = false
@State private var selectedHistoryItem: HistoryItem?
//
@State private var showImagePicker = false
@State private var isDecodingImage = false
@State private var decodedImageCodes: [DetectedCode] = []
@State private var showDecodeFailure = false
@State private var decodeFailureMessage = ""
var body: some View {
ZStack {
//
if scannerViewModel.cameraAuthorizationStatus == .authorized {
//
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
.ignoresSafeArea()
//
ScanningOverlayView(
showPreviewPause: showPreviewPause &&
!scannerViewModel.detectedCodes.isEmpty,
detectedCodesCount: scannerViewModel.detectedCodes.count,
onImageDecode: { showImagePicker = true }
)
// -
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty {
CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes,
previewLayer: previewLayer,
onCodeSelected: handleCodeSelection
)
}
// -
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty {
if let code = scannerViewModel.detectedCodes.first {
TestAutoSelectButton(
detectedCode: code,
onSelect: handleCodeSelection
)
}
}
//
if showDecodeFailure {
DecodeFailureOverlay(
message: decodeFailureMessage,
onDismiss: {
showDecodeFailure = false
}
)
}
} else {
// UI
CameraPermissionView(
authorizationStatus: scannerViewModel.cameraAuthorizationStatus,
onRequestPermission: {
scannerViewModel.refreshCameraPermission()
},
onOpenSettings: {
scannerViewModel.openSettings()
}
)
}
}
.navigationTitle("scanner_title".localized)
.navigationBarTitleDisplayMode(.inline)
.navigationBarBackButtonHidden(false)
.sheet(isPresented: $showImagePicker) {
ImagePicker(
onImageSelected: handleImageDecodeResult,
shouldProcessImage: false
)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
// -
if scannerViewModel.cameraAuthorizationStatus == .authorized && scannerViewModel.isTorchAvailable {
Button(action: {
logInfo("🔦 用户点击手电筒按钮", className: "ScannerView")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
scannerViewModel.toggleTorch()
}) {
Image(systemName: scannerViewModel.isTorchOn ? "bolt.fill" : "bolt")
.font(.system(size: 18, weight: .semibold))
.foregroundColor(scannerViewModel.isTorchOn ? .yellow : .blue)
}
}
}
ToolbarItem(placement: .navigationBarTrailing) {
// -
if showPreviewPause {
Button(action: {
logInfo("🔄 用户点击工具栏重新扫描按钮", className: "ScannerView")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
resetToScanning()
}) {
HStack(spacing: 6) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 16, weight: .semibold))
Text("rescan_button".localized)
.font(.system(size: 14, weight: .medium))
.id(languageManager.refreshTrigger)
}
.foregroundColor(.blue)
}
}
}
}
.onAppear {
//
if scannerViewModel.cameraAuthorizationStatus == .authorized {
scannerViewModel.startScanning()
}
}
.onDisappear {
scannerViewModel.stopScanning()
// 退
if scannerViewModel.isTorchOn {
scannerViewModel.turnOffTorch()
}
}
.alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) {
Button("OK") { }
} message: {
Text("scan_error_message".localized)
.id(languageManager.refreshTrigger)
}
.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()
}
.background(
NavigationLink(
destination: Group {
if let historyItem = selectedHistoryItem {
//
if historyItem.dataType == DataType.qrcode.rawValue {
QRCodeDetailView(historyItem: historyItem)
.onDisappear {
//
logInfo("🔄 从二维码详情页返回,重新开始扫描", className: "ScannerView")
resetToScanning()
}
} else {
BarcodeDetailView(historyItem: historyItem)
.onDisappear {
//
logInfo("🔄 从条形码详情页返回,重新开始扫描", className: "ScannerView")
resetToScanning()
}
}
}
},
isActive: $navigateToDetail
) {
EmptyView()
}
)
}
// MARK: -
private func handleDetectedCodes(_ codes: [DetectedCode]) {
guard !codes.isEmpty else { return }
logInfo("检测到条码数量: \(codes.count)", className: "ScannerView")
//
print("🔍 handleDetectedCodes 被调用:")
print(" 条码数量: \(codes.count)")
print(" 条码内容: \(codes.map { "\($0.type): \($0.content)" })")
print(" 条码来源: \(codes.map { $0.source })")
if codes.count == 1 {
logInfo("单个条码显示选择点并0.5秒后自动跳转", className: "ScannerView")
pauseForPreview()
// 0.5
autoSelectSingleCode(code: codes[0], delay: 0.5)
} else {
logInfo("多个条码,显示选择点等待用户选择", className: "ScannerView")
pauseForPreview()
}
}
private func handleOrientationChange() {
logInfo("Screen orientation changed", className: "ScannerView")
}
private func handleCodeSelection(_ selectedCode: DetectedCode) {
logInfo("🎯 ScannerView 收到条码选择回调", className: "ScannerView")
logInfo(" 选择的条码ID: \(selectedCode.id)", className: "ScannerView")
logInfo(" 选择的条码类型: \(selectedCode.type)", className: "ScannerView")
logInfo(" 选择的条码内容: \(selectedCode.content)", className: "ScannerView")
logInfo(" 选择的条码位置: \(selectedCode.bounds)", className: "ScannerView")
//
scannerViewModel.stopScanning()
logInfo("🛑 已停止扫描功能", className: "ScannerView")
// HistoryItem Core Data
let historyItem = createHistoryItem(from: selectedCode)
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
//
self.selectedHistoryItem = historyItem
self.navigateToDetail = true
}
//
let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)"
logInfo(" 格式化结果: \(formattedResult)", className: "ScannerView")
NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult)
}
private func createHistoryItem(from detectedCode: DetectedCode) -> HistoryItem {
let context = CoreDataManager.shared.container.viewContext
let historyItem = HistoryItem(context: context)
historyItem.id = UUID()
historyItem.content = detectedCode.content
historyItem.dataSource = DataSource.scanned.rawValue
historyItem.createdAt = Date()
historyItem.isFavorite = false
//
let isQRCode = detectedCode.type.lowercased().contains("qr") ||
detectedCode.type.lowercased().contains("二维码") ||
detectedCode.type.lowercased().contains("data matrix") ||
detectedCode.type.lowercased().contains("aztec")
if isQRCode {
//
historyItem.dataType = DataType.qrcode.rawValue
//
let parsedData = QRCodeParser.parseQRCode(detectedCode.content)
historyItem.qrCodeType = parsedData.type.rawValue
historyItem.barcodeType = nil //
logInfo("📱 创建二维码历史记录,类型: \(detectedCode.type)", className: "ScannerView")
} else {
//
historyItem.dataType = DataType.barcode.rawValue
historyItem.barcodeType = detectedCode.type
historyItem.qrCodeType = nil //
logInfo("📊 创建条形码历史记录,类型: \(detectedCode.type)", className: "ScannerView")
}
// Core Data
CoreDataManager.shared.addHistoryItem(historyItem)
logInfo("✅ 已创建并保存历史记录项", className: "ScannerView")
return historyItem
}
private func pauseForPreview() {
showPreviewPause = true
//
scannerViewModel.pauseCamera()
//
print("⏸️ pauseForPreview 被调用:")
print(" showPreviewPause: \(showPreviewPause)")
print(" detectedCodes.count: \(scannerViewModel.detectedCodes.count)")
}
private func resetToScanning() {
logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView")
// UI
showPreviewPause = false
//
scannerViewModel.resetDetection()
//
resetImageDecodeState()
//
scannerViewModel.resumeCamera()
logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView")
}
private func autoSelectSingleCode(code: DetectedCode, delay: TimeInterval = 1.0) {
logInfo("开始自动选择定时器,条码类型: \(code.type),延迟: \(delay)", className: "ScannerView")
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
let totalCodes = self.scannerViewModel.detectedCodes.count + self.decodedImageCodes.count
guard self.showPreviewPause && totalCodes == 1 else {
logInfo("条件不满足,取消自动选择", className: "ScannerView")
return
}
logInfo("条件满足,执行自动选择", className: "ScannerView")
self.handleCodeSelection(code)
}
}
// MARK: -
///
private func handleImageDecodeResult(_ image: UIImage) {
isDecodingImage = true
decodedImageCodes.removeAll()
showDecodeFailure = false
decodeFailureMessage = ""
logInfo("🔍 开始解码图片", className: "ScannerView")
// 线
DispatchQueue.global(qos: .userInitiated).async { [self] in
var allResults: [DetectedCode] = []
// 使Vision
if let cgImage = image.cgImage {
let barcodeResults = detectBarcodes(in: cgImage)
if !barcodeResults.isEmpty {
logInfo("✅ 检测到 \(barcodeResults.count) 个条码", className: "ScannerView")
allResults.append(contentsOf: barcodeResults)
}
}
// 使QRCode
if allResults.isEmpty {
let detectedQR = QRCode.DetectQRCodes(in: image)
if detectedQR.count > 0 {
logInfo("✅ 使用QRCode库检测到 \(detectedQR.count) 个二维码", className: "ScannerView")
let qrResults = detectedQR.enumerated().map { index, qrCode in
DetectedCode(
type: "QR Code",
content: qrCode.messageString ?? "unknown_content".localized,
bounds: qrCode.bounds,
source: .image
)
}
allResults.append(contentsOf: qrResults)
}
}
DispatchQueue.main.async {
if !allResults.isEmpty {
//
let uniqueResults = self.removeDuplicateCodes(allResults)
self.decodedImageCodes = uniqueResults
self.isDecodingImage = false
logInfo("✅ 图片解码完成,去重后共 \(uniqueResults.count) 个结果", className: "ScannerView")
//
if uniqueResults.count == 1 {
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.handleCodeSelection(uniqueResults[0])
}
} else if uniqueResults.count > 1 {
//
self.pauseForPreview()
logInfo("📱 图片中检测到多个条码,显示选择界面", className: "ScannerView")
}
} else {
self.isDecodingImage = false
self.decodeFailureMessage = "no_codes_detected_in_image".localized
self.showDecodeFailure = true
logWarning("❌ 图片中未检测到二维码或条形码", className: "ScannerView")
}
}
}
}
///
private func resetImageDecodeState() {
decodedImageCodes.removeAll()
showDecodeFailure = false
decodeFailureMessage = ""
}
/// 使Vision
private func detectBarcodes(in cgImage: CGImage) -> [DetectedCode] {
let request = VNDetectBarcodesRequest { request, error in
if let error = error {
logWarning("条形码检测错误: \(error.localizedDescription)", className: "ScannerView")
return
}
}
//
request.symbologies = [
.ean8,
.ean13,
.upce,
.code39,
.code39Checksum,
.code39FullASCII,
.code39FullASCIIChecksum,
.code93,
.code93i,
.code128,
.itf14,
.pdf417,
.qr,
.dataMatrix,
.aztec
]
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
do {
try handler.perform([request])
let results = request.results ?? []
guard !results.isEmpty else {
return []
}
return results.enumerated().map { index, observation in
let barcodeType = getBarcodeTypeString(from: observation.symbology)
let content = observation.payloadStringValue ?? "unknown_content".localized
logInfo("检测到条形码 #\(index + 1): 类型=\(barcodeType), 内容=\(content)", className: "ScannerView")
return DetectedCode(
type: barcodeType,
content: content,
bounds: observation.boundingBox,
source: .image
)
}
} catch {
logWarning("条形码检测请求失败: \(error.localizedDescription)", className: "ScannerView")
return []
}
}
///
private func getBarcodeTypeString(from symbology: VNBarcodeSymbology) -> String {
switch symbology {
case .ean8:
return "EAN-8"
case .ean13:
return "EAN-13"
case .upce:
return "UPC-E"
case .code39:
return "Code 39"
case .code39Checksum:
return "Code 39 (Checksum)"
case .code39FullASCII:
return "Code 39 (Full ASCII)"
case .code39FullASCIIChecksum:
return "Code 39 (Full ASCII + Checksum)"
case .code93:
return "Code 93"
case .code93i:
return "Code 93i"
case .code128:
return "Code 128"
case .itf14:
return "ITF-14"
case .pdf417:
return "PDF417"
case .qr:
return "QR Code"
case .dataMatrix:
return "Data Matrix"
case .aztec:
return "Aztec"
default:
return "Unknown Barcode"
}
}
///
private func removeDuplicateCodes(_ codes: [DetectedCode]) -> [DetectedCode] {
var uniqueCodes: [DetectedCode] = []
var seenContents: Set<String> = []
for code in codes {
if !seenContents.contains(code.content) {
seenContents.insert(code.content)
uniqueCodes.append(code)
} else {
logInfo("🔄 发现重复条码,内容: \(code.content),已跳过", className: "ScannerView")
}
}
return uniqueCodes
}
}
// MARK: -
struct DecodeFailureOverlay: View {
let message: String
let onDismiss: () -> Void
var body: some View {
ZStack {
//
Color.black.opacity(0.7)
.ignoresSafeArea()
.onTapGesture {
onDismiss()
}
//
VStack(spacing: 20) {
//
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 50))
.foregroundColor(.orange)
//
Text("decode_failed".localized)
.font(.title2)
.fontWeight(.bold)
.foregroundColor(.white)
//
Text(message)
.font(.body)
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
//
Button(action: {
onDismiss()
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 16, weight: .semibold))
Text("reselect_image".localized)
.font(.headline)
.fontWeight(.medium)
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.8))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.blue, lineWidth: 1)
)
)
}
.buttonStyle(PlainButtonStyle())
}
.padding(30)
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color(.systemGray6).opacity(0.9))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
)
.shadow(color: .black.opacity(0.3), radius: 20, x: 0, y: 10)
}
.zIndex(2000) //
.transition(.opacity.combined(with: .scale))
}
}
#if DEBUG
struct ScannerView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ScannerView()
.environmentObject(LanguageManager.shared)
}
}
}
#endif

@ -1,430 +0,0 @@
import SwiftUI
import AVFoundation
import AudioToolbox
import Combine
// MARK: -
class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
@Published var detectedCodes: [DetectedCode] = []
@Published var showAlert = false
@Published var cameraAuthorizationStatus: AVAuthorizationStatus = .notDetermined
@Published var isTorchOn = false
var captureSession: AVCaptureSession!
private var metadataOutput: AVCaptureMetadataOutput?
private var videoDevice: AVCaptureDevice?
private var isProcessingDetection = false //
override init() {
super.init()
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)
@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
}
}
}
}
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: -
private func setupCaptureSession() {
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
showAlert = true
return
}
//
videoDevice = videoCaptureDevice
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() {
logInfo("🔄 开始扫描", className: "ScannerViewModel")
//
if captureSession?.isRunning == true {
logInfo(" 扫描会话已经在运行", className: "ScannerViewModel")
return
}
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
self.captureSession?.startRunning()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self.captureSession?.isRunning == true {
logInfo("✅ 扫描会话启动成功", className: "ScannerViewModel")
} else {
logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel")
}
}
}
}
func stopScanning() {
logInfo("🔄 停止扫描", className: "ScannerViewModel")
//
isProcessingDetection = true
//
if captureSession?.isRunning == true {
//
captureSession?.stopRunning()
logInfo("✅ 扫描会话已停止", className: "ScannerViewModel")
} else {
logInfo(" 扫描会话已经停止", className: "ScannerViewModel")
}
}
///
func pauseCamera() {
logInfo("⏸️ 暂停相机功能", className: "ScannerViewModel")
//
isProcessingDetection = true
//
if captureSession?.isRunning == true {
captureSession?.stopRunning()
logInfo("✅ 相机会话已暂停", className: "ScannerViewModel")
} else {
logInfo(" 相机会话已经停止", className: "ScannerViewModel")
}
}
///
func resumeCamera() {
logInfo("▶️ 恢复相机功能", className: "ScannerViewModel")
//
guard cameraAuthorizationStatus == .authorized else {
logWarning("❌ 相机权限未授权,无法恢复相机", className: "ScannerViewModel")
return
}
//
if captureSession == nil || captureSession.inputs.isEmpty || captureSession.outputs.isEmpty {
logInfo("🔄 重新设置相机会话", className: "ScannerViewModel")
setupCaptureSession()
}
//
isProcessingDetection = false
//
if captureSession?.isRunning != true {
logInfo("🚀 启动相机会话", className: "ScannerViewModel")
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession?.startRunning()
DispatchQueue.main.async {
if self?.captureSession?.isRunning == true {
logInfo("✅ 相机会话启动成功", className: "ScannerViewModel")
} else {
logWarning("⚠️ 相机会话启动失败", className: "ScannerViewModel")
}
}
}
}
logInfo("✅ 相机功能已恢复", className: "ScannerViewModel")
}
func resetDetection() {
DispatchQueue.main.async {
logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel")
self.detectedCodes = []
self.isProcessingDetection = false //
}
}
func isSessionRunning() -> Bool {
return captureSession?.isRunning == true
}
func checkSessionStatus() {
let isRunning = captureSession?.isRunning == true
logInfo("📊 扫描会话状态检查: \(isRunning ? "运行中" : "已停止")", className: "ScannerViewModel")
if !isRunning {
logWarning("⚠️ 扫描会话未运行,尝试重新启动", className: "ScannerViewModel")
startScanning()
}
}
func restartScanning() {
logInfo("🔄 重新开始扫描", className: "ScannerViewModel")
//
if captureSession?.isRunning == true {
logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel")
captureSession?.stopRunning()
}
//
resetDetection()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel")
// 线
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession?.startRunning()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self?.captureSession?.isRunning == true {
logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel")
} else {
logWarning("⚠️ 扫描会话启动失败", className: "ScannerViewModel")
}
}
}
}
}
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
//
guard !isProcessingDetection else {
logInfo("⚠️ 正在处理检测结果,忽略新的检测", className: "ScannerViewModel")
return
}
logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel")
//
isProcessingDetection = true
//
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
//
stopScanning()
//
var codes: [DetectedCode] = []
for metadataObject in metadataObjects {
if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue {
let codeType = getBarcodeTypeString(from: readableObject.type)
let bounds = readableObject.bounds
let detectedCode = DetectedCode(
type: codeType,
content: stringValue,
bounds: bounds,
source: .camera
)
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: -
///
var isTorchAvailable: Bool {
guard let device = videoDevice else { return false }
return device.hasTorch && device.isTorchAvailable
}
///
func toggleTorch() {
guard let device = videoDevice else {
logWarning("❌ 没有可用的视频设备", className: "ScannerViewModel")
return
}
guard device.hasTorch && device.isTorchAvailable else {
logWarning("❌ 设备不支持手电筒", className: "ScannerViewModel")
return
}
do {
try device.lockForConfiguration()
if isTorchOn {
//
device.torchMode = .off
isTorchOn = false
logInfo("🔦 手电筒已关闭", className: "ScannerViewModel")
} else {
//
try device.setTorchModeOn(level: 1.0)
isTorchOn = true
logInfo("🔦 手电筒已打开", className: "ScannerViewModel")
}
device.unlockForConfiguration()
} catch {
logError("❌ 手电筒控制失败: \(error.localizedDescription)", className: "ScannerViewModel")
device.unlockForConfiguration()
}
}
///
func turnOffTorch() {
guard let device = videoDevice else { return }
do {
try device.lockForConfiguration()
device.torchMode = .off
isTorchOn = false
device.unlockForConfiguration()
logInfo("🔦 手电筒已关闭", className: "ScannerViewModel")
} catch {
logError("❌ 关闭手电筒失败: \(error.localizedDescription)", className: "ScannerViewModel")
device.unlockForConfiguration()
}
}
// MARK: -
///
private func getBarcodeTypeString(from metadataType: AVMetadataObject.ObjectType) -> String {
switch metadataType {
case .ean8:
return "EAN-8"
case .ean13:
return "EAN-13"
case .upce:
return "UPC-E"
case .code39:
return "Code 39"
case .code93:
return "Code 93"
case .code128:
return "Code 128"
case .itf14:
return "ITF-14"
case .pdf417:
return "PDF417"
case .qr:
return "QR Code"
case .dataMatrix:
return "Data Matrix"
case .aztec:
return "Aztec"
default:
//
let typeString = metadataType.rawValue
if typeString.contains("org.gs1.") {
// org.gs1.
let cleanType = typeString.replacingOccurrences(of: "org.gs1.", with: "")
return cleanType.uppercased()
}
return typeString
}
}
}

@ -1,152 +0,0 @@
import SwiftUI
// MARK: - 线
struct ScanningLineView: View {
@EnvironmentObject var languageManager: LanguageManager
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: - 线
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
}
}
func getLocalizedName(languageManager: LanguageManager) -> String {
switch self {
case .modern: return languageManager.localizedString(for: "style_modern")
case .classic: return languageManager.localizedString(for: "style_classic")
case .neon: return languageManager.localizedString(for: "style_neon")
case .minimal: return languageManager.localizedString(for: "style_minimal")
case .retro: return languageManager.localizedString(for: "style_retro")
}
}
}
// 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: - 线
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())
}
}

@ -1,220 +0,0 @@
import SwiftUI
// MARK: -
struct ScanningOverlayView: View {
let showPreviewPause: Bool
let detectedCodesCount: Int
let onImageDecode: () -> Void
var body: some View {
VStack {
Spacer()
// 线
if !showPreviewPause {
ScanningLineView(style: .modern)
}
//
ScanningInstructionView(
showPreviewPause: showPreviewPause,
detectedCodesCount: detectedCodesCount
)
Spacer()
//
ScanningBottomButtonsView(
showPreviewPause: showPreviewPause,
onImageDecode: onImageDecode
)
}
}
}
// MARK: -
struct ScanningInstructionView: View {
@EnvironmentObject var languageManager: LanguageManager
let showPreviewPause: Bool
let detectedCodesCount: Int
var body: some View {
if showPreviewPause {
VStack(spacing: 8) {
Text("detected_codes".localized)
.foregroundColor(.white)
.font(.headline)
.id(languageManager.refreshTrigger)
if detectedCodesCount == 1 {
Text("auto_result_1s".localized)
.foregroundColor(.green)
.font(.subheadline)
.id(languageManager.refreshTrigger)
} else {
Text("select_code_instruction".localized)
.foregroundColor(.white.opacity(0.8))
.font(.subheadline)
.id(languageManager.refreshTrigger)
}
}
.padding(.top, 20)
} else {
Text("scan_instruction".localized)
.foregroundColor(.white)
.font(.headline)
.padding(.top, 20)
.id(languageManager.refreshTrigger)
}
}
}
// MARK: -
struct ScanningBottomButtonsView: View {
let showPreviewPause: Bool
let onImageDecode: () -> Void
var body: some View {
VStack(spacing: 15) {
//
if !showPreviewPause {
Button(action: {
onImageDecode()
}) {
HStack(spacing: 8) {
Image(systemName: "photo.on.rectangle.angled")
.font(.system(size: 16, weight: .semibold))
Text("image_decode".localized)
.font(.subheadline)
.fontWeight(.medium)
}
.foregroundColor(.white)
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue.opacity(0.3))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(Color.blue.opacity(0.6), lineWidth: 1)
)
)
}
.buttonStyle(PlainButtonStyle())
}
// 使
}
.padding(.bottom, 50)
}
}
// MARK: - 线
struct ScanningStyleSelectorView: View {
@EnvironmentObject var languageManager: LanguageManager
@Binding var selectedStyle: ScanningLineStyle
var body: some View {
VStack(spacing: 12) {
//
Text("scanning_line_style".localized)
.font(.caption)
.foregroundColor(.white.opacity(0.8))
.padding(.bottom, 4)
//
HStack(spacing: 8) {
ForEach(ScanningLineStyle.allCases, id: \.self) { style in
Button(action: {
withAnimation(.easeInOut(duration: 0.2)) {
selectedStyle = style
}
//
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
}) {
VStack(spacing: 4) {
//
stylePreview(style)
.frame(width: 24, height: 24)
//
Text(style.getLocalizedName(languageManager: languageManager))
.font(.caption2)
.foregroundColor(.white)
.id(languageManager.refreshTrigger)
}
.frame(width: 60, height: 50)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(selectedStyle == style ?
Color.green.opacity(0.8) :
Color.black.opacity(0.6))
.overlay(
RoundedRectangle(cornerRadius: 12)
.stroke(selectedStyle == style ?
Color.green :
Color.white.opacity(0.3),
lineWidth: selectedStyle == style ? 2 : 1)
)
)
}
.buttonStyle(PlainButtonStyle())
}
}
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(Color.black.opacity(0.7))
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color.white.opacity(0.2), lineWidth: 1)
)
)
.padding(.bottom, 10)
}
//
@ViewBuilder
private func stylePreview(_ style: ScanningLineStyle) -> some View {
switch style {
case .modern:
Rectangle()
.fill(
LinearGradient(
colors: [.blue, .cyan, .blue],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: 20, height: 2)
.shadow(color: .blue, radius: 2, x: 0, y: 0)
case .classic:
Rectangle()
.fill(Color.green)
.frame(width: 16, height: 2)
case .neon:
Rectangle()
.fill(Color.purple)
.frame(width: 18, height: 3)
.shadow(color: .purple, radius: 3, x: 0, y: 0)
case .minimal:
Rectangle()
.fill(Color.white)
.frame(width: 14, height: 1)
case .retro:
Rectangle()
.fill(Color.orange)
.frame(width: 20, height: 2)
.overlay(
Rectangle()
.stroke(Color.yellow, lineWidth: 0.5)
.frame(width: 18, height: 1.5)
)
}
}
}

@ -1,26 +0,0 @@
import SwiftUI
// MARK: -
struct TestAutoSelectButton: View {
@EnvironmentObject var languageManager: LanguageManager
let detectedCode: DetectedCode
let onSelect: (DetectedCode) -> Void
var body: some View {
VStack {
HStack {
Spacer()
Button("test_auto_select".localized) {
onSelect(detectedCode)
}
.id(languageManager.refreshTrigger)
.foregroundColor(.white)
.padding(8)
.background(Color.red)
.cornerRadius(8)
.padding(.trailing, 20)
}
Spacer()
}
}
}

@ -0,0 +1,285 @@
# 项目结构重构文档
## 概述
本文档记录了MyQrCode应用的项目结构重构过程将相关功能的view按类型分组到同一个组中提高代码的可维护性和可读性。
## 重构前的结构
```
MyQrCode/Views/
├── Components/ # 通用组件
├── ScannerView/ # 扫描相关(旧结构)
├── CreateCodeView.swift
├── CreateQRCodeView.swift
├── QRCodeStyleView.swift
├── CodeContentInputView.swift
├── CodeTypeSelectionView.swift
├── ImageComposerView.swift
├── HistoryView.swift
├── QRCodeDetailView.swift
├── QRCodeSavedView.swift
├── BarcodeDetailView.swift
├── BarcodePreviewView.swift
├── BarcodeCharacterHintView.swift
├── BarcodeValidationInfoView.swift
├── SettingsView.swift
├── AppPermissionsView.swift
├── PrivacyPolicyView.swift
├── LaunchScreenView.swift
└── 其他文件...
```
## 重构后的结构
```
MyQrCode/
├── Core/ # 核心文件
│ ├── MyQrCodeApp.swift
│ └── ContentView.swift
├── Managers/ # 管理器文件
│ ├── LanguageManager.swift
│ └── Logger.swift
├── Views/ # 视图文件
│ ├── Scanner/ # 扫描相关功能
│ │ ├── ScannerView.swift
│ │ ├── ScannerViewModel.swift
│ │ ├── CameraPermissionView.swift
│ │ ├── CameraPreviewView.swift
│ │ ├── ScanningOverlayView.swift
│ │ ├── CodePositionOverlay.swift
│ │ ├── ScanningLineView.swift
│ │ ├── Models.swift
│ │ └── TestAutoSelectButton.swift
│ ├── Generator/ # 生成相关功能
│ │ ├── CreateCodeView.swift
│ │ ├── CreateQRCodeView.swift
│ │ ├── QRCodeStyleView.swift
│ │ ├── CodeContentInputView.swift
│ │ ├── CodeTypeSelectionView.swift
│ │ └── ImageComposerView.swift
│ ├── History/ # 历史记录相关功能
│ │ ├── HistoryView.swift
│ │ ├── QRCodeDetailView.swift
│ │ ├── QRCodeSavedView.swift
│ │ ├── BarcodeDetailView.swift
│ │ ├── BarcodePreviewView.swift
│ │ ├── BarcodeCharacterHintView.swift
│ │ └── BarcodeValidationInfoView.swift
│ ├── Settings/ # 设置相关功能
│ │ ├── SettingsView.swift
│ │ ├── AppPermissionsView.swift
│ │ ├── PrivacyPolicyView.swift
│ │ └── LanguageSettingsView.swift
│ ├── Components/ # 通用组件
│ │ ├── CardView.swift
│ │ ├── FormView.swift
│ │ ├── InputFieldView.swift
│ │ ├── ListView.swift
│ │ ├── PickerView.swift
│ │ ├── QRCodePreviewView.swift
│ │ ├── ValidationView.swift
│ │ ├── UtilityFunctions.swift
│ │ ├── InputComponentFactory.swift
│ │ ├── InputTitleView.swift
│ │ ├── InputHintView.swift
│ │ ├── KeyboardToolbarView.swift
│ │ ├── TextInputView.swift
│ │ ├── TextEditorView.swift
│ │ ├── URLInputView.swift
│ │ ├── EmailInputView.swift
│ │ ├── PhoneInputView.swift
│ │ ├── ContactInputView.swift
│ │ ├── LocationInputView.swift
│ │ ├── WiFiInputView.swift
│ │ ├── SocialInputView.swift
│ │ ├── CalendarInputView.swift
│ │ └── DatePickerView.swift
│ └── Utils/ # 工具类
│ └── LaunchScreenView.swift
├── Models/ # 数据模型
├── Utils/ # 工具类
├── Resources/ # 资源文件
├── Assets.xcassets/ # 资源文件
├── Docs/ # 文档文件
│ ├── MULTILINGUAL_FIX_SUMMARY.md
│ └── CODE_OPTIMIZATION_SUMMARY.md
├── Info.plist # 应用配置
└── *.lproj/ # 本地化文件
```
## 分组原则
### 1. Core组
**功能**: 应用核心文件
**包含文件**:
- `MyQrCodeApp.swift` - 应用入口点
- `ContentView.swift` - 主内容视图
### 2. Managers组
**功能**: 应用管理器
**包含文件**:
- `LanguageManager.swift` - 语言管理器
- `Logger.swift` - 日志管理器
### 3. Views组
**功能**: 所有视图文件,按功能进一步分组
#### 3.1 Scanner组
**功能**: 扫描二维码和条形码
**包含文件**:
- `ScannerView.swift` - 主扫描界面
- `ScannerViewModel.swift` - 扫描逻辑管理
- `CameraPermissionView.swift` - 相机权限界面
- `CameraPreviewView.swift` - 相机预览界面
- `ScanningOverlayView.swift` - 扫描覆盖层
- `CodePositionOverlay.swift` - 代码位置覆盖层
- `ScanningLineView.swift` - 扫描线动画
- `Models.swift` - 扫描相关数据模型
- `TestAutoSelectButton.swift` - 测试自动选择按钮
#### 3.2 Generator组
**功能**: 生成二维码和条形码
**包含文件**:
- `CreateCodeView.swift` - 创建代码主界面
- `CreateQRCodeView.swift` - 创建二维码界面
- `QRCodeStyleView.swift` - 二维码样式选择界面
- `CodeContentInputView.swift` - 代码内容输入界面
- `CodeTypeSelectionView.swift` - 代码类型选择界面
- `ImageComposerView.swift` - 图片合成界面
#### 3.3 History组
**功能**: 历史记录管理
**包含文件**:
- `HistoryView.swift` - 历史记录主界面
- `QRCodeDetailView.swift` - 二维码详情界面
- `QRCodeSavedView.swift` - 二维码保存界面
- `BarcodeDetailView.swift` - 条形码详情界面
- `BarcodePreviewView.swift` - 条形码预览界面
- `BarcodeCharacterHintView.swift` - 条形码字符提示界面
- `BarcodeValidationInfoView.swift` - 条形码验证信息界面
#### 3.4 Settings组
**功能**: 应用设置
**包含文件**:
- `SettingsView.swift` - 设置主界面
- `AppPermissionsView.swift` - 应用权限界面
- `PrivacyPolicyView.swift` - 隐私政策界面
- `LanguageSettingsView.swift` - 语言设置界面
#### 3.5 Components组
**功能**: 通用UI组件
**包含文件**: 所有可复用的UI组件
#### 3.6 Utils组
**功能**: 视图工具类
**包含文件**:
- `LaunchScreenView.swift` - 启动页面
### 4. 其他组
- **Models**: 数据模型文件
- **Utils**: 工具类文件
- **Resources**: 资源文件
- **Docs**: 文档文件
- **Assets.xcassets**: 资源文件
- **Info.plist**: 应用配置
- ***.lproj**: 本地化文件
## 重构优势
### 1. 提高可维护性
- **功能分组**: 相关功能文件集中在一起,便于查找和维护
- **职责清晰**: 每个组都有明确的职责范围
- **减少耦合**: 不同功能组之间的依赖关系更清晰
### 2. 提高可读性
- **结构清晰**: 新开发者可以快速理解项目结构
- **导航便利**: 在IDE中可以更容易地导航到相关文件
- **文档化**: 通过文件夹结构自动形成文档
### 3. 便于扩展
- **模块化**: 每个功能组可以独立开发和测试
- **可复用**: 通用组件可以在不同功能组中复用
- **可扩展**: 新增功能时可以很容易地确定放置位置
## 技术实现
### 文件移动
使用命令行工具进行文件移动:
```bash
# 创建新的目录结构
mkdir -p MyQrCode/Core
mkdir -p MyQrCode/Managers
mkdir -p MyQrCode/Views/{Scanner,Generator,History,Settings,Utils}
mkdir -p MyQrCode/Docs
# 移动核心文件
mv MyQrCode/MyQrCodeApp.swift MyQrCode/Core/
mv MyQrCode/ContentView.swift MyQrCode/Core/
# 移动管理器文件
mv MyQrCode/LanguageManager.swift MyQrCode/Managers/
mv MyQrCode/Logger.swift MyQrCode/Managers/
# 移动视图文件
mv MyQrCode/Views/ScannerView/* MyQrCode/Views/Scanner/
mv MyQrCode/Views/CreateCodeView.swift MyQrCode/Views/Generator/
mv MyQrCode/Views/CreateQRCodeView.swift MyQrCode/Views/Generator/
mv MyQrCode/Views/QRCodeStyleView.swift MyQrCode/Views/Generator/
mv MyQrCode/Views/CodeContentInputView.swift MyQrCode/Views/Generator/
mv MyQrCode/Views/CodeTypeSelectionView.swift MyQrCode/Views/Generator/
mv MyQrCode/Views/ImageComposerView.swift MyQrCode/Views/Generator/
mv MyQrCode/Views/HistoryView.swift MyQrCode/Views/History/
mv MyQrCode/Views/QRCodeDetailView.swift MyQrCode/Views/History/
mv MyQrCode/Views/QRCodeSavedView.swift MyQrCode/Views/History/
mv MyQrCode/Views/BarcodeDetailView.swift MyQrCode/Views/History/
mv MyQrCode/Views/BarcodePreviewView.swift MyQrCode/Views/History/
mv MyQrCode/Views/BarcodeCharacterHintView.swift MyQrCode/Views/History/
mv MyQrCode/Views/BarcodeValidationInfoView.swift MyQrCode/Views/History/
mv MyQrCode/Views/SettingsView.swift MyQrCode/Views/Settings/
mv MyQrCode/Views/AppPermissionsView.swift MyQrCode/Views/Settings/
mv MyQrCode/Views/PrivacyPolicyView.swift MyQrCode/Views/Settings/
mv MyQrCode/LanguageSettingsView.swift MyQrCode/Views/Settings/
mv MyQrCode/Views/LaunchScreenView.swift MyQrCode/Views/Utils/
# 移动文档文件
mv MyQrCode/MULTILINGUAL_FIX_SUMMARY.md MyQrCode/Docs/
mv MyQrCode/CODE_OPTIMIZATION_SUMMARY.md MyQrCode/Docs/
```
### 引用更新
由于Swift的模块系统会自动解析文件路径大部分引用不需要手动更新。但为了确保项目结构清晰建议
1. **保持import语句简洁**: 不需要显式import每个子目录
2. **使用相对路径**: Swift会自动处理文件引用
3. **更新文档**: 确保文档反映新的项目结构
## 验证步骤
### 1. 编译验证
```bash
xcodebuild -project MyQrCode.xcodeproj -scheme MyQrCode -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' build
```
### 2. 功能测试
- 扫描功能正常工作
- 生成功能正常工作
- 历史记录功能正常工作
- 设置功能正常工作
- 启动页面正常显示
### 3. 导航测试
- 所有NavigationLink正常工作
- 页面跳转无错误
- 返回功能正常
## 总结
通过这次项目结构重构MyQrCode应用获得了
- **更清晰的项目结构**: 按功能分组,便于理解和维护
- **更好的代码组织**: 相关文件集中,减少查找时间
- **更高的可扩展性**: 新功能可以很容易地集成到现有结构中
- **更好的团队协作**: 开发者可以快速定位相关代码
这种结构化的组织方式为项目的长期维护和扩展奠定了良好的基础。
Loading…
Cancel
Save