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 @Binding var torchOn: 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 controller.setTorch(on: torchOn) } 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) } } } /// Licht-Schalter fuer die Scan-Bildschirme. struct TorchButton: View { @Binding var isOn: Bool var body: some View { Button { isOn.toggle() } label: { Image(systemName: isOn ? "bolt.fill" : "bolt.slash.fill") } .accessibilityLabel(isOn ? "Licht ausschalten" : "Licht einschalten") } } 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? private var device: AVCaptureDevice? /// Auch krummere Symbologien mitnehmen: ITF-14 kommt auf Umkartons vor, /// DataMatrix auf kleinen Aufklebern. private static let wantedTypes: [AVMetadataObject.ObjectType] = [ .ean13, .ean8, .upce, .code128, .code39, .code93, .itf14, .interleaved2of5, .dataMatrix, .pdf417, .aztec, .qr, ] override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(focusOnTap))) AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in guard granted else { return } DispatchQueue.main.async { self?.configure() } } } private func configure() { let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) ?? AVCaptureDevice.default(for: .video) guard let camera, let input = try? AVCaptureDeviceInput(device: camera), session.canAddInput(input) else { return } device = camera session.beginConfiguration() // Mehr Pixel: kleine oder kontrastarme Codes werden sonst nicht sauber aufgeloest. if session.canSetSessionPreset(.hd1920x1080) { session.sessionPreset = .hd1920x1080 } else if session.canSetSessionPreset(.high) { session.sessionPreset = .high } session.addInput(input) let output = AVCaptureMetadataOutput() guard session.canAddOutput(output) else { session.commitConfiguration() return } session.addOutput(output) output.setMetadataObjectsDelegate(self, queue: .main) // Nur Typen setzen, die diese Kamera wirklich kann - sonst wirft AVFoundation. output.metadataObjectTypes = Self.wantedTypes.filter { output.availableMetadataObjectTypes.contains($0) } session.commitConfiguration() tuneForBarcodes(camera) let layer = AVCaptureVideoPreviewLayer(session: session) layer.videoGravity = .resizeAspectFill layer.frame = view.bounds view.layer.addSublayer(layer) preview = layer start() } /// Barcodes liegen dicht vor der Linse und sind manchmal schwach im Kontrast /// (z. B. dunkelgruen auf weiss). Nahbereich und staendiger Autofokus helfen. private func tuneForBarcodes(_ camera: AVCaptureDevice) { guard (try? camera.lockForConfiguration()) != nil else { return } if camera.isFocusModeSupported(.continuousAutoFocus) { camera.focusMode = .continuousAutoFocus } if camera.isAutoFocusRangeRestrictionSupported { camera.autoFocusRangeRestriction = .near } if camera.isExposureModeSupported(.continuousAutoExposure) { camera.exposureMode = .continuousAutoExposure } // Weiches Nachfuehren ist fuer Video gedacht und verzoegert hier nur. if camera.isSmoothAutoFocusSupported { camera.isSmoothAutoFocusEnabled = false } camera.unlockForConfiguration() } /// Antippen stellt gezielt auf den Code scharf - Rettungsanker fuer Codes, /// die der Autofokus nicht von selbst findet. @objc private func focusOnTap(_ gesture: UITapGestureRecognizer) { guard let device, let preview else { return } let point = preview.captureDevicePointConverted(fromLayerPoint: gesture.location(in: view)) guard (try? device.lockForConfiguration()) != nil else { return } if device.isFocusPointOfInterestSupported, device.isFocusModeSupported(.autoFocus) { device.focusPointOfInterest = point device.focusMode = .autoFocus } if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.continuousAutoExposure) { device.exposurePointOfInterest = point device.exposureMode = .continuousAutoExposure } device.unlockForConfiguration() } /// Licht an: hebt den Kontrast bei matten oder farbigen Codes deutlich. func setTorch(on: Bool) { guard let device, device.hasTorch, device.isTorchAvailable else { return } guard device.torchMode != (on ? .on : .off) else { return } guard (try? device.lockForConfiguration()) != nil else { return } device.torchMode = on ? .on : .off device.unlockForConfiguration() } 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) setTorch(on: false) // Licht nie versehentlich anlassen 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) } }