You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1059 lines
38 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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)
}
}
}
// 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)
}
}
// MARK: -
struct TestAutoSelectButton: View {
let detectedCode: DetectedCode
let onSelect: (DetectedCode) -> Void
var body: some View {
VStack {
HStack {
Spacer()
Button("test_auto_select".localized) {
onSelect(detectedCode)
}
.foregroundColor(.white)
.padding(8)
.background(Color.red)
.cornerRadius(8)
.padding(.trailing, 20)
}
Spacer()
}
}
}
// MARK: -
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
@Binding var previewLayer: AVCaptureVideoPreviewLayer?
func makeUIView(context: Context) -> UIView {
let view = UIView()
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
previewLayer.frame = uiView.bounds
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
}
}
}
// MARK: -
class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
@Published var detectedCodes: [DetectedCode] = []
@Published var showAlert = false
@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
}
}
}
// 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)
}
}
// MARK: - 线
struct ScanningLineModifier: ViewModifier {
@State private var isAnimating = false
func body(content: Content) -> some View {
content
.offset(y: isAnimating ? 150 : -150)
.onAppear {
withAnimation(
Animation.linear(duration: 2)
.repeatForever(autoreverses: false)
) {
isAnimating = true
}
}
}
}
// MARK: -
struct PulseAnimationModifier: ViewModifier {
@State private var isPulsing = false
func body(content: Content) -> some View {
content
.scaleEffect(isPulsing ? 1.5 : 1.0)
.opacity(isPulsing ? 0.0 : 0.8)
.onAppear {
withAnimation(
Animation.easeInOut(duration: 1.5)
.repeatForever(autoreverses: false)
) {
isPulsing = true
}
}
}
}
// MARK: - 线
enum ScanningLineStyle: String, CaseIterable {
case modern = "style_modern"
case classic = "style_classic"
case neon = "style_neon"
case minimal = "style_minimal"
case retro = "style_retro"
var localizedName: String {
switch self {
case .modern: return "style_modern".localized
case .classic: return "style_classic".localized
case .neon: return "style_neon".localized
case .minimal: return "style_minimal".localized
case .retro: return "style_retro".localized
}
}
}
// MARK: - 线
struct ScanningLineView: View {
let style: ScanningLineStyle
var body: some View {
Group {
switch style {
case .modern:
ModernScanningLine()
case .classic:
ClassicScanningLine()
case .neon:
NeonScanningLine()
case .minimal:
MinimalScanningLine()
case .retro:
RetroScanningLine()
}
}
}
}
// MARK: - 线
struct ModernScanningLine: View {
var body: some View {
Rectangle()
.fill(
LinearGradient(
colors: [.blue, .cyan, .blue],
startPoint: .leading,
endPoint: .trailing
)
)
.frame(width: 200, height: 3)
.shadow(color: .blue, radius: 5, x: 0, y: 0)
.modifier(ScanningLineModifier())
}
}
// MARK: - 线
struct ClassicScanningLine: View {
var body: some View {
Rectangle()
.fill(Color.green)
.frame(width: 150, height: 2)
.modifier(ScanningLineModifier())
}
}
// MARK: - 线
struct NeonScanningLine: View {
var body: some View {
Rectangle()
.fill(Color.purple)
.frame(width: 180, height: 4)
.shadow(color: .purple, radius: 8, x: 0, y: 0)
.modifier(ScanningLineModifier())
}
}
// MARK: - 线
struct MinimalScanningLine: View {
var body: some View {
Rectangle()
.fill(Color.white)
.frame(width: 100, height: 1)
.modifier(ScanningLineModifier())
}
}
// MARK: - 线
struct RetroScanningLine: View {
var body: some View {
HStack(spacing: 2) {
ForEach(0..<5, id: \.self) { _ in
Rectangle()
.fill(Color.orange)
.frame(width: 2, height: 20)
}
}
.modifier(ScanningLineModifier())
}
}
// MARK: -
struct DetectedCode: Identifiable {
let id = UUID()
let type: String
let content: String
let bounds: CGRect
}
// MARK: -
extension Notification.Name {
static let scannerDidScanCode = Notification.Name("scannerDidScanCode")
}
// 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
}
}
}
#if DEBUG
struct ScannerView_Previews: PreviewProvider {
static var previews: some View {
ScannerView()
}
}
#endif