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.

478 lines
17 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
internal import Combine
//
extension Notification.Name {
static let scannerDidScanCode = Notification.Name("scannerDidScanCode")
}
//
struct DetectedCode: Identifiable {
let id = UUID()
let type: String
let content: String
let bounds: CGRect
}
struct ScannerView: View {
@StateObject private var scannerViewModel = ScannerViewModel()
@Environment(\.dismiss) private var dismiss
@State private var showPreviewPause = false
@State private var previewLayer: AVCaptureVideoPreviewLayer?
@State private var screenOrientation = UIDevice.current.orientation
var body: some View {
ZStack {
//
CameraPreviewView(session: scannerViewModel.captureSession, previewLayer: $previewLayer)
.ignoresSafeArea()
//
VStack {
Spacer()
//
ZStack {
RoundedRectangle(cornerRadius: 20)
.stroke(Color.white, lineWidth: 3)
.frame(width: 250, height: 250)
.background(Color.black.opacity(0.3))
.cornerRadius(20)
// 线
if !showPreviewPause {
Rectangle()
.fill(LinearGradient(
colors: [Color.clear, Color.green, Color.clear],
startPoint: .top,
endPoint: .bottom
))
.frame(width: 250, height: 2)
.offset(y: -125)
.animation(
Animation.linear(duration: 2)
.repeatForever(autoreverses: false),
value: UUID()
)
}
}
//
if showPreviewPause {
VStack(spacing: 8) {
Text("检测到条码")
.foregroundColor(.white)
.font(.headline)
if scannerViewModel.detectedCodes.count == 1 {
Text("1秒后自动显示结果")
.foregroundColor(.green)
.font(.subheadline)
} else {
Text("点击绿色标记选择要解码的条码")
.foregroundColor(.white.opacity(0.8))
.font(.subheadline)
}
}
.padding(.top, 20)
} else {
Text("将二维码或条形码放入框内")
.foregroundColor(.white)
.font(.headline)
.padding(.top, 20)
}
Spacer()
//
VStack(spacing: 15) {
if showPreviewPause {
//
Button("重新扫描") {
resetToScanning()
}
.foregroundColor(.white)
.padding(.horizontal, 20)
.padding(.vertical, 10)
.background(Color.blue)
.cornerRadius(20)
}
//
Button("关闭") {
dismiss()
}
.foregroundColor(.white)
.padding()
.background(Color.black.opacity(0.6))
.cornerRadius(10)
}
.padding(.bottom, 50)
}
//
if showPreviewPause && !scannerViewModel.detectedCodes.isEmpty {
CodePositionOverlay(
detectedCodes: scannerViewModel.detectedCodes,
previewLayer: previewLayer,
onCodeSelected: { selectedCode in
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
dismiss()
}
)
}
//
if showPreviewPause && scannerViewModel.detectedCodes.count == 1 {
VStack {
HStack {
Spacer()
Button("测试自动选择") {
let code = scannerViewModel.detectedCodes[0]
let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
dismiss()
}
.foregroundColor(.white)
.padding(8)
.background(Color.red)
.cornerRadius(8)
.padding(.trailing, 20)
}
Spacer()
}
}
}
.onAppear {
scannerViewModel.startScanning()
}
.onDisappear {
scannerViewModel.stopScanning()
}
.alert("扫描失败", isPresented: $scannerViewModel.showAlert) {
Button("确定") {
dismiss()
}
} message: {
Text("您的设备不支持扫描二维码。请使用带相机的设备。")
}
.onReceive(scannerViewModel.$detectedCodes) { codes in
if !codes.isEmpty {
print("检测到条码数量: \(codes.count)")
if codes.count == 1 {
//
print("单个条码,准备自动选择")
pauseForPreview()
autoSelectSingleCode(code: codes[0])
} else {
//
print("多个条码,等待用户选择")
pauseForPreview()
}
}
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
//
screenOrientation = UIDevice.current.orientation
print("Screen orientation changed to: \(screenOrientation.rawValue)")
}
}
private func pauseForPreview() {
showPreviewPause = true
//
}
private func resetToScanning() {
showPreviewPause = false
scannerViewModel.resetDetection()
scannerViewModel.startScanning()
}
private func autoSelectSingleCode(code: DetectedCode) {
print("开始自动选择定时器,条码类型: \(code.type)")
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
print("自动选择定时器触发")
print("当前状态 - showPreviewPause: \(self.showPreviewPause)")
print("当前条码数量: \(self.scannerViewModel.detectedCodes.count)")
if self.showPreviewPause && self.scannerViewModel.detectedCodes.count == 1 {
print("条件满足,执行自动选择")
//
let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
print("发送通知: \(selectedCode)")
NotificationCenter.default.post(name: .scannerDidScanCode, object: selectedCode)
self.dismiss()
} else {
print("条件不满足,取消自动选择")
}
}
}
}
//
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
@Binding var previewLayer: AVCaptureVideoPreviewLayer?
func makeUIView(context: Context) -> UIView {
let view = UIView()
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.bounds
previewLayer.videoGravity = .resizeAspectFill
view.layer.addSublayer(previewLayer)
//
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
previewLayer.frame = uiView.bounds
//
DispatchQueue.main.async {
self.previewLayer = previewLayer
}
}
}
}
//
class ScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
@Published var detectedCodes: [DetectedCode] = []
@Published var showAlert = false
var captureSession: AVCaptureSession!
private var metadataOutput: AVCaptureMetadataOutput?
override init() {
super.init()
setupCaptureSession()
}
private func setupCaptureSession() {
captureSession = AVCaptureSession()
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
showAlert = true
return
}
let videoInput: AVCaptureDeviceInput
do {
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
} catch {
showAlert = true
return
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
} else {
showAlert = true
return
}
metadataOutput = AVCaptureMetadataOutput()
if let metadataOutput = metadataOutput,
captureSession.canAddOutput(metadataOutput) {
captureSession.addOutput(metadataOutput)
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .code128, .code39, .upce, .pdf417, .aztec]
} else {
showAlert = true
return
}
}
func startScanning() {
DispatchQueue.global(qos: .background).async { [weak self] in
self?.captureSession?.startRunning()
}
}
func stopScanning() {
DispatchQueue.global(qos: .background).async { [weak self] in
self?.captureSession?.stopRunning()
}
}
func resetDetection() {
DispatchQueue.main.async {
self.detectedCodes = []
}
}
// MARK: - AVCaptureMetadataOutputObjectsDelegate
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
print("metadataOutput 被调用,检测到 \(metadataObjects.count) 个对象")
//
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
//
stopScanning()
//
var codes: [DetectedCode] = []
for metadataObject in metadataObjects {
if let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
let stringValue = readableObject.stringValue {
let codeType = readableObject.type.rawValue
let bounds = readableObject.bounds
let detectedCode = DetectedCode(
type: codeType,
content: stringValue,
bounds: bounds
)
codes.append(detectedCode)
print("创建 DetectedCode: 类型=\(codeType), 内容=\(stringValue)")
}
}
print("准备更新 detectedCodes数量: \(codes.count)")
//
DispatchQueue.main.async {
print("在主线程更新 detectedCodes")
self.detectedCodes = codes
}
}
}
//
struct CodePositionOverlay: View {
let detectedCodes: [DetectedCode]
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (String) -> Void
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(detectedCodes) { code in
CodePositionMarker(
code: code,
screenSize: geometry.size,
previewLayer: previewLayer,
onCodeSelected: onCodeSelected
)
}
}
}
.allowsHitTesting(true) //
}
}
//
struct CodePositionMarker: View {
let code: DetectedCode
let screenSize: CGSize
let previewLayer: AVCaptureVideoPreviewLayer?
let onCodeSelected: (String) -> Void
var body: some View {
GeometryReader { geometry in
// 使GeometryReader
let position = calculatePosition(screenSize: geometry.size)
ZStack {
//
Circle()
.stroke(Color.green, lineWidth: 3)
.frame(width: 40, height: 40)
//
Circle()
.fill(Color.green.opacity(0.3))
.frame(width: 20, height: 20)
//
Circle()
.fill(Color.green)
.frame(width: 6, height: 6)
}
.position(x: position.x, y: position.y)
.background(
//
Circle()
.fill(Color.clear)
.frame(width: 60, height: 60)
)
.onTapGesture {
//
let selectedCode = "类型: \(code.type)\n内容: \(code.content)"
onCodeSelected(selectedCode)
}
.onAppear {
print("CodePositionMarker appeared at: x=\(position.x), y=\(position.y)")
print("Screen size: \(geometry.size)")
print("Code bounds: \(code.bounds)")
}
}
}
private func calculatePosition(screenSize: CGSize) -> CGPoint {
guard let previewLayer = previewLayer else {
// 使
print("No preview layer available, using screen center")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
//
guard previewLayer.session?.isRunning == true else {
print("Preview layer session not running, using screen center")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
// 使AVFoundation
let metadataObject = code.bounds
let convertedPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: CGPoint(
x: metadataObject.midX,
y: metadataObject.midY
))
//
guard convertedPoint.x.isFinite && convertedPoint.y.isFinite else {
print("Invalid converted point: \(convertedPoint), using screen center")
return CGPoint(x: screenSize.width / 2, y: screenSize.height / 2)
}
//
let clampedX = max(20, min(screenSize.width - 20, convertedPoint.x))
let clampedY = max(20, min(screenSize.height - 20, convertedPoint.y))
print("AVFoundation bounds: \(code.bounds)")
print("Converted point: \(convertedPoint)")
print("Screen size: \(screenSize)")
print("Clamped: x=\(clampedX), y=\(clampedY)")
return CGPoint(x: clampedX, y: clampedY)
}
}
#Preview {
ScannerView()
}