Files
bookibra/ios/Bookibra/Views/AddBooks/BarcodeScannerView.swift

155 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import AVFoundation
import VisionKit
struct BarcodeScannerView: View {
let onScanned: (String) -> Void
var body: some View {
Group {
if DataScannerViewController.isSupported, DataScannerViewController.isAvailable {
DataScannerRepresentable(onScanned: onScanned)
} else {
AVScannerRepresentable(onScanned: onScanned)
}
}
.overlay {
RoundedRectangle(cornerRadius: 16)
.stroke(Color.white.opacity(0.75), lineWidth: 2)
}
.accessibilityLabel("ISBN barkod tarayıcı")
}
}
@available(iOS 16.0, *)
private struct DataScannerRepresentable: UIViewControllerRepresentable {
let onScanned: (String) -> Void
func makeCoordinator() -> Coordinator { Coordinator(onScanned: onScanned) }
func makeUIViewController(context: Context) -> DataScannerViewController {
let vc = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.ean8, .ean13, .upce, .code128])],
qualityLevel: .balanced,
recognizesMultipleItems: false,
isHighFrameRateTrackingEnabled: true,
isPinchToZoomEnabled: true,
isGuidanceEnabled: true,
isHighlightingEnabled: true
)
vc.delegate = context.coordinator
try? vc.startScanning()
return vc
}
func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {}
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
let onScanned: (String) -> Void
private var lastISBN: String?
private var lastEmitAt: Date = .distantPast
init(onScanned: @escaping (String) -> Void) {
self.onScanned = onScanned
}
func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
guard case .barcode(let code) = item,
let payload = code.payloadStringValue,
let normalized = ISBNNormalizer.normalize(payload) else { return }
emitIfNeeded(normalized)
}
func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
guard let first = addedItems.first,
case .barcode(let code) = first,
let payload = code.payloadStringValue,
let normalized = ISBNNormalizer.normalize(payload) else { return }
emitIfNeeded(normalized)
}
private func emitIfNeeded(_ isbn: String) {
let now = Date()
if lastISBN == isbn, now.timeIntervalSince(lastEmitAt) < 1.5 {
return
}
lastISBN = isbn
lastEmitAt = now
onScanned(isbn)
}
}
}
private struct AVScannerRepresentable: UIViewRepresentable {
let onScanned: (String) -> Void
func makeUIView(context: Context) -> ScannerPreviewView {
let view = ScannerPreviewView()
context.coordinator.configure(preview: view)
return view
}
func updateUIView(_ uiView: ScannerPreviewView, context: Context) {}
func makeCoordinator() -> Coordinator { Coordinator(onScanned: onScanned) }
final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate {
private let session = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "bookibra.av.capture.session")
private let onScanned: (String) -> Void
private var lastISBN: String?
private var lastEmitAt: Date = .distantPast
init(onScanned: @escaping (String) -> Void) {
self.onScanned = onScanned
}
func configure(preview: ScannerPreviewView) {
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else { return }
if session.canAddInput(input) { session.addInput(input) }
let output = AVCaptureMetadataOutput()
if session.canAddOutput(output) {
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.metadataObjectTypes = [.ean8, .ean13, .upce, .code128]
}
preview.previewLayer.session = session
preview.previewLayer.videoGravity = .resizeAspectFill
sessionQueue.async { [session] in
session.startRunning()
}
}
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
guard let code = metadataObjects.compactMap({ $0 as? AVMetadataMachineReadableCodeObject }).first,
let value = code.stringValue,
let normalized = ISBNNormalizer.normalize(value) else { return }
let now = Date()
if lastISBN == normalized, now.timeIntervalSince(lastEmitAt) < 1.5 {
return
}
lastISBN = normalized
lastEmitAt = now
onScanned(normalized)
}
}
}
private final class ScannerPreviewView: UIView {
override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }
}
enum ISBNNormalizer {
static func normalize(_ value: String) -> String? {
let cleaned = value.uppercased().filter { $0.isNumber || $0 == "X" }
if cleaned.count == 13 { return cleaned }
if cleaned.count == 10 { return cleaned }
return nil
}
}