iOS: Name/Marke per Text scannen + Frischedatum ohne Jahr erkennen
- Neuer TextScanView: beim Anlegen Name und Marke per Texterkennung einlesen (erkannte Zeilen antippen, wie beim MHD-Scan) - kein Abtippen mehr, wenn ein Produkt nicht in Open Food/Products Facts steht. - BestBeforeText erkennt jetzt auch Tag.Monat ohne Jahr (z.B. "05.01" bei Frischware / "zu verbrauchen bis") und ergaenzt das aktuelle Jahr (bzw. das naechste, falls schon vergangen). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -81,6 +81,136 @@ struct DateScanView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest eine Textzeile (Name/Marke) per Texterkennung ein – die erkannten
|
||||
/// Zeilen erscheinen zum Antippen, wie beim MHD-Scan. Nützlich, wenn ein Produkt
|
||||
/// nicht in Open Food/Products Facts steht und man nicht abtippen will.
|
||||
struct TextScanView: View {
|
||||
let titel: String
|
||||
var onPick: (String) -> Void
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var zeilen: [String] = []
|
||||
|
||||
static var isSupported: Bool {
|
||||
DataScannerViewController.isSupported && DataScannerViewController.isAvailable
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .bottom) {
|
||||
if Self.isSupported {
|
||||
TextLinesScannerRepresentable { erkannt in
|
||||
for t in erkannt.reversed() {
|
||||
let sauber = t.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard sauber.count >= 2, !zeilen.contains(sauber) else { continue }
|
||||
zeilen.insert(sauber, at: 0)
|
||||
}
|
||||
if zeilen.count > 8 { zeilen = Array(zeilen.prefix(8)) }
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
} else {
|
||||
Color.black.ignoresSafeArea()
|
||||
Text("Dieses Gerät unterstützt keine Texterkennung.")
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
if zeilen.isEmpty {
|
||||
hinweis("Text in den Sucher halten und die passende Zeile antippen.")
|
||||
} else {
|
||||
ScrollView {
|
||||
VStack(spacing: 8) {
|
||||
ForEach(zeilen, id: \.self) { zeile in
|
||||
Button {
|
||||
onPick(zeile)
|
||||
dismiss()
|
||||
} label: {
|
||||
HStack {
|
||||
Text(zeile).font(.headline).lineLimit(2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
}
|
||||
.padding()
|
||||
.background(.thinMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle(titel)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wie TextScannerRepresentable, meldet aber die einzelnen erkannten Zeilen
|
||||
/// (statt sie zu einem String zu verketten) – für die Zeilenauswahl.
|
||||
private struct TextLinesScannerRepresentable: UIViewControllerRepresentable {
|
||||
var onLines: ([String]) -> Void
|
||||
|
||||
func makeCoordinator() -> Coordinator { Coordinator(onLines: onLines) }
|
||||
|
||||
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 onLines: ([String]) -> Void
|
||||
init(onLines: @escaping ([String]) -> Void) { self.onLines = onLines }
|
||||
|
||||
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 lines = items.compactMap { item -> String? in
|
||||
if case .text(let erkannt) = item { return erkannt.transcript }
|
||||
return nil
|
||||
}
|
||||
guard !lines.isEmpty else { return }
|
||||
onLines(lines)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Duenne Huelle um den Text-Scanner von VisionKit.
|
||||
private struct TextScannerRepresentable: UIViewControllerRepresentable {
|
||||
var onText: (String) -> Void
|
||||
|
||||
Reference in New Issue
Block a user