Remove ScannerView and its associated components; this includes the camera handling, UI elements, and scanning logic, streamlining the project by eliminating unused code.

main
v504 2 months ago
parent 1fcc3dbbc0
commit fd18b7b683

File diff suppressed because it is too large Load Diff

@ -0,0 +1,81 @@
import SwiftUI
import AVFoundation
// MARK: -
struct CameraPermissionView: View {
let authorizationStatus: AVAuthorizationStatus
let onRequestPermission: () -> Void
let onOpenSettings: () -> Void
var body: some View {
VStack(spacing: 30) {
Spacer()
//
Image(systemName: "camera.fill")
.font(.system(size: 80))
.foregroundColor(.gray)
//
Text("camera_permission_title".localized)
.font(.largeTitle)
.fontWeight(.bold)
.multilineTextAlignment(.center)
//
Text(getDescriptionText())
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
.padding(.horizontal, 40)
//
VStack(spacing: 15) {
if authorizationStatus == .notDetermined {
Button(action: onRequestPermission) {
HStack {
Image(systemName: "camera.badge.ellipsis")
Text("request_camera_permission".localized)
}
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.cornerRadius(12)
}
} else if authorizationStatus == .denied || authorizationStatus == .restricted {
Button(action: onOpenSettings) {
HStack {
Image(systemName: "gear")
Text("open_settings".localized)
}
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange)
.cornerRadius(12)
}
}
}
.padding(.horizontal, 40)
Spacer()
}
.background(Color(.systemBackground))
}
private func getDescriptionText() -> String {
switch authorizationStatus {
case .notDetermined:
return "camera_permission_description".localized
case .denied:
return "camera_permission_denied".localized
case .restricted:
return "camera_permission_restricted".localized
default:
return "camera_permission_unknown".localized
}
}
}

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

@ -0,0 +1,213 @@
import SwiftUI
import AVFoundation
// MARK: -
struct CodePositionOverlay: View {
let detectedCodes: [DetectedCode]
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (DetectedCode) -> Void
let onRescan: () -> Void
var body: some View {
GeometryReader { geometry in
ZStack {
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
// -
VStack {
HStack {
Spacer()
Button(action: {
logInfo("🔄 用户点击重新扫描按钮", className: "CodePositionOverlay")
//
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
impactFeedback.impactOccurred()
onRescan()
}) {
HStack(spacing: 8) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 18, weight: .semibold))
.rotationEffect(.degrees(0))
.animation(.easeInOut(duration: 0.3), value: true)
Text("rescan_button".localized)
.font(.system(size: 15, weight: .semibold))
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 25)
.fill(Color.blue.opacity(0.9))
.shadow(color: .black.opacity(0.3), radius: 4, x: 0, y: 2)
)
}
.buttonStyle(RescanButtonStyle())
.padding(.trailing, 25)
.padding(.top, 25)
//
#if DEBUG
Button(action: {
logInfo("🔍 调试:检查扫描会话状态", className: "CodePositionOverlay")
//
}) {
Image(systemName: "info.circle")
.font(.system(size: 16, weight: .medium))
.foregroundColor(.yellow)
.padding(8)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
.padding(.trailing, 10)
.padding(.top, 25)
#endif
}
Spacer()
}
}
}
.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)
}
guard previewLayer.session?.isRunning == true else {
logWarning("Preview layer session not running, using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
let metadataObject = code.bounds
let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint(
x: metadataObject.midX,
y: metadataObject.midY
))
guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else {
logWarning("Invalid converted point: \(convertedPoint), using screen center", className: "CodePositionMarker")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x))
let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y))
logDebug("AVFoundation bounds: \(code.bounds)", className: "CodePositionMarker")
logDebug("Converted point: \(convertedPoint)", className: "CodePositionMarker")
logDebug("Screen size: \(screenSize)", className: "CodePositionMarker")
logDebug("Clamped: x=\(clampedX), y=\(clampedY)", className: "CodePositionMarker")
return CGPoint(x: clampedX, y: clampedY)
}
}
// MARK: -
struct 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)
}
}

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

@ -0,0 +1,170 @@
import SwiftUI
import AVFoundation
import AudioToolbox
import Combine
// MARK: -
struct ScannerView: View {
@StateObject private var scannerViewModel = ScannerViewModel()
@State private var showPreviewPause = false
@State private var selectedScanningStyle: ScanningLineStyle = .modern
@State private var screenOrientation = UIDevice.current.orientation
@State private var previewLayer: AVCaptureVideoPreviewLayer?
@Environment(\.dismiss) private var dismiss
var body: some View {
ZStack {
//
if scannerViewModel.cameraAuthorizationStatus == .authorized {
//
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
.ignoresSafeArea()
//
ScanningOverlayView(
showPreviewPause: showPreviewPause,
selectedStyle: $selectedScanningStyle,
detectedCodesCount: scannerViewModel.detectedCodes.count,
onClose: { dismiss() }
)
//
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty {
CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes,
previewLayer: previewLayer,
onCodeSelected: handleCodeSelection,
onRescan: resetToScanning
)
}
//
if showPreviewPause && scannerViewModel.detectedCodes.count == 1 {
TestAutoSelectButton(
detectedCode: scannerViewModel.detectedCodes[0],
onSelect: handleCodeSelection
)
}
} else {
// UI
CameraPermissionView(
authorizationStatus: scannerViewModel.cameraAuthorizationStatus,
onRequestPermission: {
scannerViewModel.refreshCameraPermission()
},
onOpenSettings: {
scannerViewModel.openSettings()
}
)
}
}
.onAppear {
//
if scannerViewModel.cameraAuthorizationStatus == .authorized {
scannerViewModel.startScanning()
}
}
.onDisappear {
scannerViewModel.stopScanning()
}
.alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) {
Button("OK") { }
} message: {
Text("scan_error_message".localized)
}
.onReceive(scannerViewModel.$detectedCodes) { codes in
handleDetectedCodes(codes)
}
.onReceive(scannerViewModel.$cameraAuthorizationStatus) { status in
if status == .authorized {
logInfo("🎯 相机权限已授权,启动扫描", className: "ScannerView")
scannerViewModel.startScanning()
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
handleOrientationChange()
}
}
// MARK: -
private func handleDetectedCodes(_ codes: [DetectedCode]) {
guard !codes.isEmpty else { return }
logInfo("检测到条码数量: \(codes.count)", className: "ScannerView")
if codes.count == 1 {
logInfo("单个条码,准备自动选择", className: "ScannerView")
pauseForPreview()
autoSelectSingleCode(code: codes[0])
} else {
logInfo("多个条码,等待用户选择", className: "ScannerView")
pauseForPreview()
}
}
private func handleOrientationChange() {
screenOrientation = UIDevice.current.orientation
logInfo("Screen orientation changed to: \(screenOrientation.rawValue)", className: "ScannerView")
}
private func handleCodeSelection(_ selectedCode: DetectedCode) {
logInfo("🎯 ScannerView 收到条码选择回调", className: "ScannerView")
logInfo(" 选择的条码ID: \(selectedCode.id)", className: "ScannerView")
logInfo(" 选择的条码类型: \(selectedCode.type)", className: "ScannerView")
logInfo(" 选择的条码内容: \(selectedCode.content)", className: "ScannerView")
logInfo(" 选择的条码位置: \(selectedCode.bounds)", className: "ScannerView")
let formattedResult = "类型: \(selectedCode.type)\n内容: \(selectedCode.content)"
logInfo(" 格式化结果: \(formattedResult)", className: "ScannerView")
NotificationCenter.default.post(name: .scannerDidScanCode, object: formattedResult)
dismiss()
}
private func pauseForPreview() {
showPreviewPause = true
}
private func resetToScanning() {
logInfo("🔄 ScannerView 开始重置到扫描状态", className: "ScannerView")
// UI
showPreviewPause = false
//
scannerViewModel.resetDetection()
scannerViewModel.restartScanning()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
logInfo("🔍 检查扫描会话状态", className: "ScannerView")
self.scannerViewModel.checkSessionStatus()
}
logInfo("✅ ScannerView 已重置到扫描状态", className: "ScannerView")
}
private func autoSelectSingleCode(code: DetectedCode) {
logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
guard self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 else {
logInfo("条件不满足,取消自动选择", className: "ScannerView")
return
}
logInfo("条件满足,执行自动选择", className: "ScannerView")
self.handleCodeSelection(code)
}
}
}
#if DEBUG
struct ScannerView_Previews: PreviewProvider {
static var previews: some View {
ScannerView()
}
}
#endif

@ -0,0 +1,280 @@
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 showPermissionAlert = false
var captureSession: AVCaptureSession!
private var metadataOutput: AVCaptureMetadataOutput?
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)
showPermissionAlert = true
@unknown default:
logWarning("❓ 未知的相机权限状态", className: "ScannerViewModel")
cameraAuthorizationStatus = .notDetermined
}
}
private func requestCameraPermission() {
logInfo("🔐 请求相机权限", className: "ScannerViewModel")
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
DispatchQueue.main.async {
if granted {
logInfo("✅ 相机权限请求成功", className: "ScannerViewModel")
self?.cameraAuthorizationStatus = .authorized
self?.setupCaptureSession()
} else {
logWarning("❌ 相机权限请求被拒绝", className: "ScannerViewModel")
self?.cameraAuthorizationStatus = .denied
self?.showPermissionAlert = true
}
}
}
}
func openSettings() {
logInfo("⚙️ 打开系统设置", className: "ScannerViewModel")
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(settingsUrl) { success in
if success {
logInfo("✅ 成功打开系统设置", className: "ScannerViewModel")
} else {
logWarning("⚠️ 打开系统设置失败", className: "ScannerViewModel")
}
}
}
}
func refreshCameraPermission() {
logInfo("🔍 重新检查相机权限状态", className: "ScannerViewModel")
checkCameraPermission()
}
// MARK: -
private func setupCaptureSession() {
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
showAlert = true
return
}
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
showAlert = true
return
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
} else {
showAlert = true
return
}
metadataOutput = AVCaptureMetadataOutput()
if let metadataOutput = metadataOutput,
captureSession.canAddOutput(metadataOutput) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .code128, .code39, .upce, .pdf417, .aztec]
} else {
showAlert = true
return
}
}
// MARK: -
func startScanning() {
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")
//
if captureSession?.isRunning == true {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.captureSession?.stopRunning()
DispatchQueue.main.async {
logInfo("✅ 扫描会话已停止", className: "ScannerViewModel")
}
}
} else {
logInfo(" 扫描会话已经停止", className: "ScannerViewModel")
}
}
func resetDetection() {
DispatchQueue.main.async {
logInfo("🔄 重置检测状态,清空 detectedCodes", className: "ScannerViewModel")
self.detectedCodes = []
}
}
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")
// 线UI
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
//
if self.captureSession?.isRunning == true {
logInfo("🔄 停止当前运行的扫描会话", className: "ScannerViewModel")
self.captureSession?.stopRunning()
}
//
self.detectedCodes = []
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
logInfo("🔄 准备重新启动扫描会话", className: "ScannerViewModel")
// 线
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession?.startRunning()
//
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
if self.captureSession?.isRunning == true {
logInfo("✅ 扫描会话已成功重新启动", className: "ScannerViewModel")
} else {
logWarning("⚠️ 扫描会话启动失败,尝试重新启动", className: "ScannerViewModel")
//
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession?.startRunning()
DispatchQueue.main.async {
if self.captureSession?.isRunning == true {
logInfo("✅ 扫描会话第二次尝试启动成功", className: "ScannerViewModel")
} else {
logError("❌ 扫描会话启动失败", className: "ScannerViewModel")
}
}
}
}
}
}
}
}
}
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
logInfo("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象", className: "ScannerViewModel")
//
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
//
stopScanning()
//
var codes: [DetectedCode] = []
for metadataObject in metadataObjects {
if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue {
let codeType = readableObject.type.rawValue
let bounds = readableObject.bounds
let detectedCode = DetectedCode(
type: codeType,
content: stringValue,
bounds: bounds
)
codes.append(detectedCode)
logInfo("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)", className: "ScannerViewModel")
}
}
logInfo("准备更新 detectedCodes数量: \(codes.count)", className: "ScannerViewModel")
//
DispatchQueue.main.async {
logInfo("在主线程更新 detectedCodes", className: "ScannerViewModel")
self.detectedCodes = codes
}
}
}

@ -0,0 +1,141 @@
import SwiftUI
// MARK: - 线
struct ScanningLineView: View {
let style: ScanningLineStyle
var body: some View {
Group {
switch style {
case .modern:
ModernScanningLine()
case .classic:
ClassicScanningLine()
case .neon:
NeonScanningLine()
case .minimal:
MinimalScanningLine()
case .retro:
RetroScanningLine()
}
}
}
}
// MARK: - 线
enum ScanningLineStyle: String, CaseIterable {
case modern = "style_modern"
case classic = "style_classic"
case neon = "style_neon"
case minimal = "style_minimal"
case retro = "style_retro"
var localizedName: String {
switch self {
case .modern: return "style_modern".localized
case .classic: return "style_classic".localized
case .neon: return "style_neon".localized
case .minimal: return "style_minimal".localized
case .retro: return "style_retro".localized
}
}
}
// MARK: - 线
struct 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())
}
}

@ -0,0 +1,117 @@
import SwiftUI
// MARK: -
struct ScanningOverlayView: View {
let showPreviewPause: Bool
@Binding var selectedStyle: ScanningLineStyle
let detectedCodesCount: Int
let onClose: () -> Void
var body: some View {
VStack {
Spacer()
// 线
if !showPreviewPause {
ScanningLineView(style: selectedStyle)
}
//
ScanningInstructionView(
showPreviewPause: showPreviewPause,
detectedCodesCount: detectedCodesCount
)
Spacer()
//
ScanningBottomButtonsView(
showPreviewPause: showPreviewPause,
selectedStyle: $selectedStyle,
onClose: onClose
)
}
}
}
// MARK: -
struct ScanningInstructionView: View {
let showPreviewPause: Bool
let detectedCodesCount: Int
var body: some View {
if showPreviewPause {
VStack(spacing: 8) {
Text("detected_codes".localized)
.foregroundColor(.white)
.font(.headline)
if detectedCodesCount == 1 {
Text("auto_result_1s".localized)
.foregroundColor(.green)
.font(.subheadline)
} else {
Text("select_code_instruction".localized)
.foregroundColor(.white.opacity(0.8))
.font(.subheadline)
}
}
.padding(.top, 20)
} else {
Text("scan_instruction".localized)
.foregroundColor(.white)
.font(.headline)
.padding(.top, 20)
}
}
}
// MARK: -
struct ScanningBottomButtonsView: View {
let showPreviewPause: Bool
@Binding var selectedStyle: ScanningLineStyle
let onClose: () -> Void
var body: some View {
VStack(spacing: 15) {
// 线
if !showPreviewPause {
ScanningStyleSelectorView(selectedStyle: $selectedStyle)
}
// -
if !showPreviewPause {
Button("close_button".localized) {
onClose()
}
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.6))
.cornerRadius(10)
}
}
.padding(.bottom, 50)
}
}
// MARK: - 线
struct ScanningStyleSelectorView: View {
@Binding var selectedStyle: ScanningLineStyle
var body: some View {
HStack(spacing: 10) {
ForEach(ScanningLineStyle.allCases, id: \.self) { style in
Button(style.localizedName) {
selectedStyle = style
}
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(selectedStyle == style ? Color.green : Color.gray.opacity(0.6))
.cornerRadius(8)
.font(.caption)
}
}
.padding(.bottom, 10)
}
}

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

@ -0,0 +1,203 @@
# ScannerView 文件分离重构说明
## 🎯 重构目标
将原本单一的 `ScannerView.swift` 文件中的各个类分离到单独的文件中,并统一放到 `ScannerView/` 文件夹中,提高代码的可维护性和可读性。
## 📁 重构后的文件结构
```
MyQrCode/ScannerView/
├── ScannerView.swift # 主扫描视图
├── ScannerViewModel.swift # 扫描器视图模型
├── ScanningOverlayView.swift # 扫描界面覆盖层
├── ScanningLineView.swift # 扫描线相关视图和样式
├── CodePositionOverlay.swift # 条码位置标记覆盖层
├── CameraPreviewView.swift # 相机预览视图
├── CameraPermissionView.swift # 相机权限视图
├── TestAutoSelectButton.swift # 测试自动选择按钮
└── Models.swift # 数据模型和扩展
```
## 🔧 分离的类和组件
### 1. **ScannerView.swift** - 主扫描视图
- **主要职责**: 扫描视图的主要结构和逻辑
- **包含内容**:
- 主视图结构
- 权限状态检查
- 事件处理方法
- 状态管理
### 2. **ScannerViewModel.swift** - 扫描器视图模型
- **主要职责**: 扫描器的核心业务逻辑
- **包含内容**:
- 相机权限管理
- 扫描会话控制
- 条码检测处理
- 状态同步
### 3. **ScanningOverlayView.swift** - 扫描界面覆盖层
- **主要职责**: 扫描界面的UI覆盖层
- **包含内容**:
- `ScanningOverlayView`: 主覆盖层结构
- `ScanningInstructionView`: 扫描指令显示
- `ScanningBottomButtonsView`: 底部按钮区域
- `ScanningStyleSelectorView`: 扫描线样式选择器
### 4. **ScanningLineView.swift** - 扫描线相关视图
- **主要职责**: 扫描线的样式和动画
- **包含内容**:
- `ScanningLineView`: 扫描线主视图
- `ScanningLineStyle`: 扫描线样式枚举
- `ScanningLineModifier`: 扫描线动画修饰符
- `PulseAnimationModifier`: 脉冲动画修饰符
- 各种扫描线样式实现(现代、经典、霓虹、极简、复古)
### 5. **CodePositionOverlay.swift** - 条码位置标记
- **主要职责**: 条码位置标记和交互
- **包含内容**:
- `CodePositionOverlay`: 条码位置标记覆盖层
- `CodePositionMarker`: 单个条码位置标记
- `RescanButtonStyle`: 重新扫描按钮样式
### 6. **CameraPreviewView.swift** - 相机预览视图
- **主要职责**: 相机预览的UI包装
- **包含内容**:
- `CameraPreviewView`: UIViewRepresentable 包装器
### 7. **CameraPermissionView.swift** - 相机权限视图
- **主要职责**: 相机权限相关的UI
- **包含内容**:
- `CameraPermissionView`: 权限状态显示和操作
### 8. **TestAutoSelectButton.swift** - 测试按钮
- **主要职责**: 调试用的自动选择测试按钮
- **包含内容**:
- `TestAutoSelectButton`: 测试按钮视图
### 9. **Models.swift** - 数据模型
- **主要职责**: 数据结构和扩展
- **包含内容**:
- `DetectedCode`: 检测到的条码数据结构
- `Notification.Name` 扩展: 通知名称定义
## 🚀 重构的优势
### 1. **代码组织性**
- 每个文件都有明确的职责
- 相关的功能被组织在一起
- 文件大小更加合理
### 2. **可维护性**
- 修改特定功能时只需要关注对应文件
- 减少了文件冲突的可能性
- 代码更容易理解和调试
### 3. **可重用性**
- 各个组件可以独立使用
- 便于在其他项目中复用
- 组件间的依赖关系更清晰
### 4. **团队协作**
- 不同开发者可以同时修改不同文件
- 代码审查更加聚焦
- 减少了合并冲突
### 5. **测试友好**
- 可以独立测试各个组件
- 单元测试更容易编写
- 测试覆盖率更容易提高
## 📋 重构检查清单
- ✅ 主扫描视图分离
- ✅ 视图模型分离
- ✅ 扫描覆盖层分离
- ✅ 扫描线相关组件分离
- ✅ 条码位置标记分离
- ✅ 相机预览视图分离
- ✅ 相机权限视图分离
- ✅ 测试按钮分离
- ✅ 数据模型分离
- ✅ 原文件删除
- ✅ 项目编译通过
## 🔍 文件依赖关系
```
ScannerView.swift
├── ScannerViewModel.swift
├── ScanningOverlayView.swift
├── CodePositionOverlay.swift
├── CameraPreviewView.swift
├── CameraPermissionView.swift
└── TestAutoSelectButton.swift
ScanningOverlayView.swift
├── ScanningLineView.swift
└── Models.swift
CodePositionOverlay.swift
└── Models.swift
ScanningLineView.swift
└── Models.swift
```
## 🧪 测试建议
### 1. **编译测试**
- 确保所有文件都能正确编译
- 检查是否有缺失的导入语句
- 验证类型引用是否正确
### 2. **功能测试**
- 测试扫描功能是否正常
- 验证权限管理是否工作
- 检查UI组件是否正常显示
### 3. **性能测试**
- 确保文件分离没有影响性能
- 检查内存使用是否正常
- 验证启动时间是否合理
## 🚨 注意事项
### 1. **导入语句**
- 每个文件都需要正确的 import 语句
- 确保依赖关系清晰
- 避免循环依赖
### 2. **访问控制**
- 检查 public、internal、private 修饰符
- 确保组件间的访问权限正确
- 避免过度暴露内部实现
### 3. **文件命名**
- 文件名应该清晰表达其内容
- 遵循 Swift 命名规范
- 保持命名的一致性
## 🎯 后续优化建议
### 1. **进一步模块化**
- 考虑将相关组件打包成独立的模块
- 使用 Swift Package Manager 管理依赖
- 创建组件库供其他项目使用
### 2. **文档完善**
- 为每个组件添加详细的文档注释
- 创建使用示例和最佳实践
- 提供组件使用指南
### 3. **测试覆盖**
- 为每个组件编写单元测试
- 添加集成测试
- 实现自动化测试流程
## 📊 重构总结
通过这次重构,我们成功地将原本单一的 `ScannerView.swift` 文件分离成了9个独立的文件每个文件都有明确的职责和功能。这样的重构大大提高了代码的可维护性、可读性和可重用性为后续的开发工作奠定了良好的基础。
重构后的代码结构更加清晰,组件间的依赖关系更加明确,团队协作效率得到提升。同时,这种模块化的设计也为未来的功能扩展和优化提供了更大的灵活性。
Loading…
Cancel
Save