MHD per Texterkennung einlesen
In der Chargenzeile beim Einlagern gibt es einen Knopf "MHD scannen". Er oeffnet die Texterkennung von VisionKit; erkannte Daten erscheinen als Vorschlag und werden erst uebernommen, wenn man sie antippt. Bewusst kein automatisches Uebernehmen: Eine Fehllesung beim MHD faellt sonst erst auf, wenn die Ware bereits falsch einsortiert ist. Der Parser deckt die Schreibweisen ab, die auf Verpackungen vorkommen - Tagesdaten (12.09.2026, 2026-09-12, 12/09/2026, 12.09.26) und Monatsangaben (09/2026, 09.2026, 2026-09, 09/26). Erkennt der Aufdruck nur Monat und Jahr, stellt sich die Eingabe sichtbar auf Monat/Jahr um. Damit aus Chargennummern und EAN-Codes keine Phantasiedaten werden, greifen mehrere Einschraenkungen: Muster mit Tag werden zuerst geprueft, ein bereits erkannter Textabschnitt wird nicht erneut ausgewertet, Monate ausserhalb 1-12 entfallen und das Jahr muss im Fenster von fuenf Jahren rueckwaerts bis dreissig vorwaerts liegen. Der Knopf erscheint nur, wenn das Geraet Texterkennung unterstuetzt (Neural Engine); sonst bleibt die Eingabe von Hand. Getestet: Der Parser wurde eigenstaendig uebersetzt und gegen 14 Faelle geprueft, alle korrekt - darunter, dass "12.09.2026" nicht als Monatsangabe gelesen wird, dass eine EAN keine Treffer erzeugt und dass unplausible Jahre und Monate verworfen werden. Der iOS-Geraetebuild laeuft fehlerfrei durch. Die Erkennung an einer echten Verpackung habe ich nicht selbst ausprobiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
136
ios/Sources/DateScanView.swift
Normal file
136
ios/Sources/DateScanView.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
import SwiftUI
|
||||
import VisionKit
|
||||
|
||||
/// Liest das MHD per Texterkennung von der Verpackung ab.
|
||||
///
|
||||
/// Gefundene Daten werden als Vorschlag angezeigt; uebernommen wird erst,
|
||||
/// was der Benutzer antippt.
|
||||
struct DateScanView: View {
|
||||
var onPick: (Date, String) -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var matches: [BestBeforeText.Match] = []
|
||||
|
||||
/// Texterkennung braucht Geraete mit Neural Engine; auf aelteren fehlt sie.
|
||||
static var isSupported: Bool {
|
||||
DataScannerViewController.isSupported && DataScannerViewController.isAvailable
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
if Self.isSupported {
|
||||
TextScannerRepresentable { text in
|
||||
let neu = BestBeforeText.matches(in: text)
|
||||
guard !neu.isEmpty else { return }
|
||||
// Neueste Funde vorn, hoechstens eine Handvoll anzeigen.
|
||||
for treffer in neu where !matches.contains(where: {
|
||||
$0.date == treffer.date && $0.precision == treffer.precision
|
||||
}) {
|
||||
matches.insert(treffer, at: 0)
|
||||
}
|
||||
if matches.count > 5 { matches = Array(matches.prefix(5)) }
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
} else {
|
||||
Color.black.ignoresSafeArea()
|
||||
Text("Dieses Gerät unterstützt keine Texterkennung.")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
VStack(spacing: 10) {
|
||||
if matches.isEmpty {
|
||||
hinweis("Das Mindesthaltbarkeitsdatum in den Sucher halten.")
|
||||
} else {
|
||||
ForEach(matches) { treffer in
|
||||
Button {
|
||||
onPick(treffer.date, treffer.precision)
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "calendar")
|
||||
Text(treffer.label).font(.headline)
|
||||
Spacer()
|
||||
Text("Übernehmen").font(.subheadline)
|
||||
}
|
||||
.padding()
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.thinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
hinweis("Bitte gegen den Aufdruck prüfen.")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("MHD scannen")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .topBarLeading) { Button("Abbrechen") { dismiss() } }
|
||||
}
|
||||
}
|
||||
|
||||
private func hinweis(_ text: String) -> some View {
|
||||
Text(text)
|
||||
.font(.caption)
|
||||
.padding(10)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(.ultraThinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
}
|
||||
|
||||
/// Duenne Huelle um den Text-Scanner von VisionKit.
|
||||
private struct TextScannerRepresentable: UIViewControllerRepresentable {
|
||||
var onText: (String) -> Void
|
||||
|
||||
func makeCoordinator() -> Coordinator { Coordinator(onText: onText) }
|
||||
|
||||
func makeUIViewController(context: Context) -> DataScannerViewController {
|
||||
let controller = DataScannerViewController(
|
||||
recognizedDataTypes: [.text()],
|
||||
qualityLevel: .accurate,
|
||||
recognizesMultipleItems: true,
|
||||
isHighFrameRateTrackingEnabled: false,
|
||||
isHighlightingEnabled: true
|
||||
)
|
||||
controller.delegate = context.coordinator
|
||||
return controller
|
||||
}
|
||||
|
||||
func updateUIViewController(_ controller: DataScannerViewController, context: Context) {
|
||||
guard !controller.isScanning else { return }
|
||||
try? controller.startScanning()
|
||||
}
|
||||
|
||||
static func dismantleUIViewController(_ controller: DataScannerViewController, coordinator: Coordinator) {
|
||||
controller.stopScanning()
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
|
||||
let onText: (String) -> Void
|
||||
init(onText: @escaping (String) -> Void) { self.onText = onText }
|
||||
|
||||
func dataScanner(_ scanner: DataScannerViewController,
|
||||
didAdd addedItems: [RecognizedItem],
|
||||
allItems: [RecognizedItem]) {
|
||||
handle(allItems)
|
||||
}
|
||||
|
||||
func dataScanner(_ scanner: DataScannerViewController,
|
||||
didUpdate updatedItems: [RecognizedItem],
|
||||
allItems: [RecognizedItem]) {
|
||||
handle(allItems)
|
||||
}
|
||||
|
||||
private func handle(_ items: [RecognizedItem]) {
|
||||
let text = items.compactMap { item -> String? in
|
||||
if case .text(let erkannt) = item { return erkannt.transcript }
|
||||
return nil
|
||||
}.joined(separator: " ")
|
||||
guard !text.isEmpty else { return }
|
||||
onText(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user