From 8e94f15c8462dbdaac5ee4552369f37399fa994a Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Wed, 22 Jul 2026 17:28:21 +0200 Subject: [PATCH] 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 --- ios/AppIcon.icon/Assets/barcode-gelesen.svg | 1 + ios/AppIcon.icon/icon.json | 37 +++++++ ios/Sources/CheckInView.swift | 5 +- ios/Sources/CheckOutView.swift | 5 +- ios/Sources/RootView.swift | 4 +- ios/Sources/ScannerView.swift | 105 +++++++++++++++++++- ios/project.yml | 3 + 7 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 ios/AppIcon.icon/Assets/barcode-gelesen.svg create mode 100644 ios/AppIcon.icon/icon.json diff --git a/ios/AppIcon.icon/Assets/barcode-gelesen.svg b/ios/AppIcon.icon/Assets/barcode-gelesen.svg new file mode 100644 index 0000000..8420535 --- /dev/null +++ b/ios/AppIcon.icon/Assets/barcode-gelesen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ios/AppIcon.icon/icon.json b/ios/AppIcon.icon/icon.json new file mode 100644 index 0000000..a2cc60d --- /dev/null +++ b/ios/AppIcon.icon/icon.json @@ -0,0 +1,37 @@ +{ + "fill" : { + "automatic-gradient" : "srgb:0.55985,0.47253,1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "glass" : false, + "image-name" : "barcode-gelesen.svg", + "name" : "barcode-gelesen", + "position" : { + "scale" : 1.4, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/ios/Sources/CheckInView.swift b/ios/Sources/CheckInView.swift index 0e886ce..6d33e29 100644 --- a/ios/Sources/CheckInView.swift +++ b/ios/Sources/CheckInView.swift @@ -4,6 +4,7 @@ struct CheckInView: View { @Environment(\.dismiss) private var dismiss @State private var paused = false + @State private var torchOn = false @State private var status: String? @State private var error: String? @@ -18,7 +19,8 @@ struct CheckInView: View { var body: some View { ZStack(alignment: .bottom) { - ScannerView(onCode: { code in Task { await resolve(code) } }, isPaused: $paused) + ScannerView(onCode: { code in Task { await resolve(code) } }, + isPaused: $paused, torchOn: $torchOn) .ignoresSafeArea() VStack(spacing: 10) { @@ -63,6 +65,7 @@ struct CheckInView: View { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } + ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn) } } .task { units = (try? await APIClient.shared.units()) ?? [] diff --git a/ios/Sources/CheckOutView.swift b/ios/Sources/CheckOutView.swift index 5286cf9..1566751 100644 --- a/ios/Sources/CheckOutView.swift +++ b/ios/Sources/CheckOutView.swift @@ -4,6 +4,7 @@ struct CheckOutView: View { @Environment(\.dismiss) private var dismiss @State private var paused = false + @State private var torchOn = false @State private var status: String? @State private var error: String? @State private var product: Product? @@ -14,7 +15,8 @@ struct CheckOutView: View { var body: some View { ZStack(alignment: .bottom) { - ScannerView(onCode: { code in Task { await resolve(code) } }, isPaused: $paused) + ScannerView(onCode: { code in Task { await resolve(code) } }, + isPaused: $paused, torchOn: $torchOn) .ignoresSafeArea() VStack(spacing: 10) { @@ -48,6 +50,7 @@ struct CheckOutView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } + ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn) } } .task { units = (try? await APIClient.shared.units()) ?? [] } .alert("EAN eingeben", isPresented: $manualCodeShown) { diff --git a/ios/Sources/RootView.swift b/ios/Sources/RootView.swift index d3f52f6..ef74e6e 100644 --- a/ios/Sources/RootView.swift +++ b/ios/Sources/RootView.swift @@ -44,8 +44,10 @@ struct HomeView: View { Button { router.route = .checkout } label: { + // "arrow.up.from.line" gibt es als SF Symbol nicht - das Icon + // blieb dadurch leer und es war nur die Kachelflaeche zu sehen. ActionTile(title: "Auslagern", subtitle: "Barcode scannen und Menge entnehmen", - systemImage: "arrow.up.from.line") + systemImage: "arrow.up.to.line") } Spacer() } diff --git a/ios/Sources/ScannerView.swift b/ios/Sources/ScannerView.swift index 120f293..31bef8f 100644 --- a/ios/Sources/ScannerView.swift +++ b/ios/Sources/ScannerView.swift @@ -6,6 +6,7 @@ import SwiftUI struct ScannerView: UIViewControllerRepresentable { var onCode: (String) -> Void @Binding var isPaused: Bool + @Binding var torchOn: Bool func makeCoordinator() -> Coordinator { Coordinator(onCode: onCode) } @@ -17,6 +18,7 @@ struct ScannerView: UIViewControllerRepresentable { func updateUIViewController(_ controller: ScannerViewController, context: Context) { context.coordinator.isPaused = isPaused + controller.setTorch(on: torchOn) } final class Coordinator: NSObject, ScannerViewControllerDelegate { @@ -39,6 +41,20 @@ struct ScannerView: UIViewControllerRepresentable { } } +/// 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) } @@ -48,10 +64,19 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje 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() } @@ -59,16 +84,36 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje } private func configure() { - guard let device = AVCaptureDevice.default(for: .video), - let input = try? AVCaptureDeviceInput(device: device), + 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 { return } + guard session.canAddOutput(output) else { + session.commitConfiguration() + return + } session.addOutput(output) output.setMetadataObjectsDelegate(self, queue: .main) - output.metadataObjectTypes = [.ean13, .ean8, .upce, .code128, .code39, .qr] + // 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 @@ -79,6 +124,52 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje 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() } @@ -90,7 +181,11 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated); start() } - override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated); stop() } + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + setTorch(on: false) // Licht nie versehentlich anlassen + stop() + } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() diff --git a/ios/project.yml b/ios/project.yml index b860d2a..e9a5e92 100644 --- a/ios/project.yml +++ b/ios/project.yml @@ -21,12 +21,15 @@ targets: - path: Sources excludes: - Info.plist + # App-Icon im Icon-Composer-Format (Xcode 26). + - path: AppIcon.icon settings: base: INFOPLIST_FILE: Sources/Info.plist PRODUCT_BUNDLE_IDENTIFIER: com.scarriffle.projectgood # Signing: automatisch verwaltet. Wer mit einem anderen Apple-Konto baut, # traegt hier seine Team-ID ein (Xcode: Signing & Capabilities -> Team). + ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: PP34X97WS3 MARKETING_VERSION: "1.0"