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"