import AVFoundation import SwiftUI /// Live-Kamerabild mit Barcode-Erkennung (EAN-8/13, UPC-E, Code128, QR). /// Meldet jeden erkannten Code einmal – erst nach `resume()` wieder. struct ScannerView: UIViewControllerRepresentable { var onCode: (String) -> Void @Binding var isPaused: Bool func makeCoordinator() -> Coordinator { Coordinator(onCode: onCode) } func makeUIViewController(context: Context) -> ScannerViewController { let controller = ScannerViewController() controller.delegate = context.coordinator return controller } func updateUIViewController(_ controller: ScannerViewController, context: Context) { context.coordinator.isPaused = isPaused } final class Coordinator: NSObject, ScannerViewControllerDelegate { let onCode: (String) -> Void var isPaused: Bool = false private var lastCode: String? private var lastTime: Date = .distantPast init(onCode: @escaping (String) -> Void) { self.onCode = onCode } func scanner(_ controller: ScannerViewController, didFind code: String) { guard !isPaused else { return } // Entprellen: derselbe Code nicht mehrfach in kurzer Folge. if code == lastCode, Date().timeIntervalSince(lastTime) < 2 { return } lastCode = code lastTime = Date() AudioServicesPlaySystemSound(1057) onCode(code) } } } protocol ScannerViewControllerDelegate: AnyObject { func scanner(_ controller: ScannerViewController, didFind code: String) } final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { weak var delegate: ScannerViewControllerDelegate? private let session = AVCaptureSession() private var preview: AVCaptureVideoPreviewLayer? override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in guard granted else { return } DispatchQueue.main.async { self?.configure() } } } private func configure() { guard let device = AVCaptureDevice.default(for: .video), let input = try? AVCaptureDeviceInput(device: device), session.canAddInput(input) else { return } session.addInput(input) let output = AVCaptureMetadataOutput() guard session.canAddOutput(output) else { return } session.addOutput(output) output.setMetadataObjectsDelegate(self, queue: .main) output.metadataObjectTypes = [.ean13, .ean8, .upce, .code128, .code39, .qr] let layer = AVCaptureVideoPreviewLayer(session: session) layer.videoGravity = .resizeAspectFill layer.frame = view.bounds view.layer.addSublayer(layer) preview = layer start() } func start() { guard !session.isRunning else { return } DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.session.startRunning() } } func stop() { guard session.isRunning else { return } DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.session.stopRunning() } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated); start() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated); stop() } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() preview?.frame = view.bounds } func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let value = object.stringValue else { return } delegate?.scanner(self, didFind: value) } }