iOS-App angefangen; Import-Modi; MHD-Formatierung; Gebinde in Ablauf-Ansichten
iOS (neu, ios/):
- SwiftUI-App: Login (Server + Keychain-Token), Kamera-Scanner (EAN/UPC/Code128),
Einlagern mit mehreren Chargen und eigenem MHD, Auslagern mit Chargenauswahl
oder FEFO, Artikelsuche, Anlegen mit Open-Food-Facts-Vorbefuellung.
- Home-Screen-Shortcuts: Schnellaktionen (langer Druck) und URL-Schema
projectgood://checkin | ://checkout fuer eigene Symbole via Kurzbefehle.
- project.yml (XcodeGen) + README mit Build-Anleitung. NICHT kompiliert - auf
diesem Rechner ist kein Xcode vorhanden.
Import-Modi (wie besprochen sinnvoll):
- add (Standard, nichts loeschen), replace_listed (Bestaende der in der Datei
genannten Produkte ersetzen - fuer Inventur), replace_all (alles ersetzen).
Geleerte Bestaende werden als Korrektur-Bewegung protokolliert; die Oberflaeche
fragt bei den zerstoerenden Modi nach.
Anzeige:
- "Bald ablaufend"/"Abgelaufen" zeigen jetzt das Gebinde ("1 Glas") mit der
Basiseinheit klein darunter (ExpiringItem liefert die Einheiten mit).
- MHD als Zeitspanne mit korrektem Numerus: "in 3 Tagen", "in 1 Woche",
"vor 2 Wochen", dazu heute/morgen/gestern.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
107
ios/Sources/ScannerView.swift
Normal file
107
ios/Sources/ScannerView.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .black
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
guard granted else { return }
|
||||
DispatchQueue.main.async { self?.configure() }
|
||||
}
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
guard let device = AVCaptureDevice.default(for: .video),
|
||||
let input = try? AVCaptureDeviceInput(device: device),
|
||||
session.canAddInput(input) else { return }
|
||||
session.addInput(input)
|
||||
|
||||
let output = AVCaptureMetadataOutput()
|
||||
guard session.canAddOutput(output) else { return }
|
||||
session.addOutput(output)
|
||||
output.setMetadataObjectsDelegate(self, queue: .main)
|
||||
output.metadataObjectTypes = [.ean13, .ean8, .upce, .code128, .code39, .qr]
|
||||
|
||||
let layer = AVCaptureVideoPreviewLayer(session: session)
|
||||
layer.videoGravity = .resizeAspectFill
|
||||
layer.frame = view.bounds
|
||||
view.layer.addSublayer(layer)
|
||||
preview = layer
|
||||
|
||||
start()
|
||||
}
|
||||
|
||||
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); 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user