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 } }