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>
137 lines
5.1 KiB
Swift
137 lines
5.1 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|