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.

691 lines
24 KiB

This file contains ambiguous Unicode 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 UIKit
import Combine
import AudioToolbox
//
extension Notification.Name {
static let scannerDidScanCode = Notification.Name("scannerDidScanCode")
}
//
struct DetectedCode: Identifiable {
let id = UUID()
let type: String
let content: String
let bounds: CGRect
}
struct ScannerView: View {
@StateObject private var scannerViewModel = ScannerViewModel()
@Environment(\.dismiss) private var dismiss
@State private var showPreviewPause = false
@State private var previewLayer: AVCaptureVideoPreviewLayer?
@State private var screenOrientation = UIDevice.current.orientation
@State private var selectedScanningStyle: ScanningLineStyle = .modern
var body: some View {
ZStack {
//
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
.ignoresSafeArea()
//
VStack {
Spacer()
// 线
if !showPreviewPause {
ScanningLineView(style: selectedScanningStyle)
}
//
if showPreviewPause {
VStack(spacing: 8) {
Text("detected_codes".localized)
.foregroundColor(.white)
.font(.headline)
if scannerViewModel.detectedCodes.count == 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)
}
Spacer()
//
VStack(spacing: 15) {
// 线
if !showPreviewPause {
HStack(spacing: 10) {
ForEach(ScanningLineStyle.allCases, id: \.self) { style in
Button(style.localizedName) {
selectedScanningStyle = style
}
.foregroundColor(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(selectedScanningStyle == style ? Color.green : Color.gray.opacity(0.6))
.cornerRadius(8)
.font(.caption)
}
}
.padding(.bottom, 10)
}
if showPreviewPause {
//
Button("rescan_button".localized) {
resetToScanning()
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.blue)
.cornerRadius(20)
}
//
Button("close_button".localized) {
dismiss()
}
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.6))
.cornerRadius(10)
}
.padding(.bottom, 50)
}
//
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty {
CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes,
previewLayer: previewLayer,
onCodeSelected: { selectedCode in
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
dismiss()
}
)
}
//
if showPreviewPause && scannerViewModel.detectedCodes.count == 1 {
VStack {
HStack {
Spacer()
Button("test_auto_select".localized) {
let code = scannerViewModel.detectedCodes[0]
let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
dismiss()
}
.foregroundColor(.white)
.padding(8)
.background(Color.red)
.cornerRadius(8)
.padding(.trailing, 20)
}
Spacer()
}
}
}
.onAppear {
scannerViewModel.startScanning()
}
.onDisappear {
scannerViewModel.stopScanning()
}
.alert("scan_error_title".localized, isPresented: $scannerViewModel.showAlert) {
Button("确定") {
dismiss()
}
} message: {
Text("scan_error_message".localized)
}
.onReceive(scannerViewModel.$detectedCodes) { codes in
if !codes.isEmpty {
logInfo("检测到条码数量: \(codes.count)", className: "ScannerView")
if codes.count == 1 {
//
logInfo("单个条码,准备自动选择", className: "ScannerView")
pauseForPreview()
autoSelectSingleCode(code: codes[0])
} else {
//
logInfo("多个条码,等待用户选择", className: "ScannerView")
pauseForPreview()
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
//
screenOrientation = UIDevice.current.orientation
logInfo("Screen orientation changed to: \(screenOrientation)", className: "ScannerView")
}
}
private func pauseForPreview() {
showPreviewPause = true
//
}
private func resetToScanning() {
showPreviewPause = false
scannerViewModel.resetDetection()
scannerViewModel.startScanning()
}
private func autoSelectSingleCode(code: DetectedCode) {
logInfo("开始自动选择定时器,条码类型: \(code.type)", className: "ScannerView")
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
logInfo("自动选择定时器触发", className: "ScannerView")
logInfo("当前状态 - showPreviewPause: \(self.showPreviewPause)", className: "ScannerView")
logInfo("当前条码数量: \(self.scannerViewModel.detectedCodes.count)", className: "ScannerView")
if self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 {
logInfo("条件满足,执行自动选择", className: "ScannerView")
//
let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
logInfo("发送通知: \(selectedCode)", className: "ScannerView")
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
self.dismiss()
} else {
logInfo("条件不满足,取消自动选择", className: "ScannerView")
}
}
}
}
//
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
@Binding var previewLayer: AVCaptureVideoPreviewLayer?
func makeUIView(context: Context) -> UIView {
let view = UIView()
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
//
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
previewLayer.frame = uiView.bounds
//
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
}
}
}
//
class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
@Published var detectedCodes: [DetectedCode] = []
@Published var showAlert = false
var captureSession: AVCaptureSession!
private var metadataOutput: AVCaptureMetadataOutput?
override init() {
super.init()
setupCaptureSession()
}
// 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
}
}
func startScanning() {
DispatchQueue.global(qos: .background).async { [weak self] in
self?.captureSession?.startRunning()
}
}
func stopScanning() {
DispatchQueue.global(qos: .background).async { [weak self] in
self?.captureSession?.stopRunning()
}
}
func resetDetection() {
DispatchQueue.main.async {
self.detectedCodes = []
}
}
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
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
}
}
}
//
struct CodePositionOverlay: View {
let detectedCodes: [DetectedCode]
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (String) -> Void
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(detectedCodes) { code in
CodePositionMarker(
code: code,
screenSize: geometry.size,
previewLayer: previewLayer,
onCodeSelected: onCodeSelected
)
}
}
}
.allowsHitTesting(true) //
}
}
//
struct CodePositionMarker: View {
let code: DetectedCode
let screenSize: CGSize
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (String) -> Void
var body: some View {
GeometryReader { geometry in
// 使GeometryReader
let position = calculatePosition(screenSize: geometry.size)
ZStack {
//
Circle()
.stroke(Color.green, lineWidth: 3)
.frame(width: 40, height: 40)
//
Circle()
.fill(Color.green.opacity(0.3))
.frame(width: 20, height: 20)
//
Circle()
.fill(Color.green)
.frame(width: 6, height: 6)
}
.position(x: position.x, y: position.y)
.background(
//
Circle()
.fill(Color.clear)
.frame(width: 60, height: 60)
)
.onTapGesture {
//
let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
onCodeSelected(selectedCode)
}
.onAppear {
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)
}
// 使AVFoundation
let metadataObject = code.bounds
let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint(
x: metadataObject.midX,
y: metadataObject.midY
))
//
guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else {
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)
}
}
// 线
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
}
}
}
}
//
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
}
}
}
}
// 线
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 {
return self.rawValue.localized
}
}
// 线
struct ScanningLineView: View {
let style: ScanningLineStyle
var body: some View {
switch style {
case .modern:
ModernScanningLine()
case .classic:
ClassicScanningLine()
case .neon:
NeonScanningLine()
case .minimal:
MinimalScanningLine()
case .retro:
RetroScanningLine()
}
}
}
// 线
struct ModernScanningLine: View {
var body: some View {
ZStack {
//
Rectangle()
.fill(LinearGradient(
colors: [Color.clear, Color.green.opacity(0.3), Color.clear],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 320, height: 8)
.blur(radius: 3)
// 线
Rectangle()
.fill(LinearGradient(
colors: [Color.clear, Color.green, Color.clear],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 300, height: 3)
.shadow(color: .green, radius: 2, x: 0, y: 0)
//
Rectangle()
.fill(LinearGradient(
colors: [Color.clear, Color.green.opacity(0.8), Color.clear],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 300, height: 1)
.blur(radius: 1)
//
Circle()
.fill(Color.green)
.frame(width: 8, height: 8)
.shadow(color: .green, radius: 4, x: 0, y: 0)
//
Circle()
.stroke(Color.green.opacity(0.6), lineWidth: 2)
.frame(width: 16, height: 16)
//
Circle()
.stroke(Color.green.opacity(0.4), lineWidth: 1)
.frame(width: 24, height: 24)
.scaleEffect(1.0)
.opacity(0.8)
.modifier(PulseAnimationModifier())
}
.modifier(ScanningLineModifier())
}
}
// 线
struct ClassicScanningLine: View {
var body: some View {
Rectangle()
.fill(Color.green)
.frame(width: 300, height: 2)
.modifier(ScanningLineModifier())
}
}
// 线
struct NeonScanningLine: View {
var body: some View {
ZStack {
//
Rectangle()
.fill(LinearGradient(
colors: [Color.clear, Color.cyan.opacity(0.6), Color.clear],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 340, height: 12)
.blur(radius: 4)
// 线
Rectangle()
.fill(LinearGradient(
colors: [Color.cyan, Color.blue, Color.cyan],
startPoint: .leading,
endPoint: .trailing
))
.frame(width: 300, height: 4)
.shadow(color: .cyan, radius: 6, x: 0, y: 0)
//
Circle()
.fill(Color.white)
.frame(width: 6, height: 6)
.shadow(color: .white, radius: 8, x: 0, y: 0)
}
.modifier(ScanningLineModifier())
}
}
// 线
struct MinimalScanningLine: View {
var body: some View {
Rectangle()
.fill(Color.white.opacity(0.8))
.frame(width: 280, height: 1)
.modifier(ScanningLineModifier())
}
}
// 线
struct RetroScanningLine: View {
var body: some View {
ZStack {
// 线
Rectangle()
.fill(LinearGradient(
colors: [Color.clear, Color.orange, Color.clear],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 300, height: 3)
.overlay(
Rectangle()
.stroke(Color.orange, lineWidth: 1)
.frame(width: 300, height: 3)
)
//
Circle()
.fill(Color.orange)
.frame(width: 4, height: 4)
}
.modifier(ScanningLineModifier())
}
}
#if DEBUG
struct ScannerView_Previews: PreviewProvider {
static var previews: some View {
ScannerView()
}
}
#endif