Files
Vorrania/ios/Sources/ScannerView.swift
Scarriffle 8e94f15c84 App-Icon eingebunden, fehlendes Auslagern-Symbol und Barcode-Erkennung verbessert
Auslagern-Kachel: "arrow.up.from.line" existiert als SF Symbol nicht, deshalb
blieb das Icon leer und man sah nur die eingefaerbte Kachelflaeche. Ersetzt
durch "arrow.up.to.line", das Gegenstueck zum "arrow.down.to.line" beim
Einlagern. Alle uebrigen im Projekt verwendeten Symbolnamen wurden gegen die
Symbolliste des Systems geprueft und sind gueltig.

App-Icon: Das mitgelieferte AppIcon.icon (Icon-Composer-Format) wird als
Ressource eingebunden und ueber ASSETCATALOG_COMPILER_APPICON_NAME gesetzt.
Die Icon-Eintraege landen korrekt in der selbstgepflegten Info.plist.

Barcode-Erkennung, nachdem ein dunkelgruener Code auf weiss nicht gelesen wurde:
- Aufloesung auf 1920x1080 statt der Voreinstellung, damit kontrastarme und
  kleine Codes ueberhaupt sauber aufgeloest werden.
- Dauerhafter Autofokus mit Beschraenkung auf den Nahbereich; weiches
  Nachfuehren aus, da es hier nur verzoegert.
- Antippen stellt gezielt auf eine Stelle scharf und misst dort die Belichtung.
- Licht-Schalter in der Navigationsleiste beider Scan-Bildschirme; beim
  Verlassen wird das Licht sicher wieder ausgeschaltet.
- Mehr Symbologien (ITF-14, DataMatrix, PDF417, Aztec, Code93). Es werden nur
  die Typen gesetzt, die die Kamera meldet - sonst wirft AVFoundation zur
  Laufzeit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 17:28:21 +02:00

203 lines
7.8 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 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)
}
}