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 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-22 17:28:21 +02:00
parent 7b0a78b421
commit 8e94f15c84
7 changed files with 152 additions and 8 deletions

View File

@@ -0,0 +1 @@
<svg id="Layer_1" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="m4 6h2v12h-2zm4 12h2v-12h-2zm12-12h-2v12h2zm-9 12h3v-12h-3zm-9 1v-3h-2v3c0 1.654 1.346 3 3 3h3v-2h-3c-.551 0-1-.449-1-1zm20 0c0 .551-.449 1-1 1h-3v2h3c1.654 0 3-1.346 3-3v-3h-2zm-1-17h-3v2h3c.551 0 1 .449 1 1v3h2v-3c0-1.654-1.346-3-3-3zm-21 3v3h2v-3c0-.551.449-1 1-1h3v-2h-3c-1.654 0-3 1.346-3 3z"/><path d="m16 6h-1v12h1z"/></svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -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"
}
}

View File

@@ -4,6 +4,7 @@ struct CheckInView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var paused = false @State private var paused = false
@State private var torchOn = false
@State private var status: String? @State private var status: String?
@State private var error: String? @State private var error: String?
@@ -18,7 +19,8 @@ struct CheckInView: View {
var body: some View { var body: some View {
ZStack(alignment: .bottom) { 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() .ignoresSafeArea()
VStack(spacing: 10) { VStack(spacing: 10) {
@@ -63,6 +65,7 @@ struct CheckInView: View {
ToolbarItem(placement: .topBarLeading) { ToolbarItem(placement: .topBarLeading) {
Button("Fertig") { dismiss() } Button("Fertig") { dismiss() }
} }
ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn) }
} }
.task { .task {
units = (try? await APIClient.shared.units()) ?? [] units = (try? await APIClient.shared.units()) ?? []

View File

@@ -4,6 +4,7 @@ struct CheckOutView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var paused = false @State private var paused = false
@State private var torchOn = false
@State private var status: String? @State private var status: String?
@State private var error: String? @State private var error: String?
@State private var product: Product? @State private var product: Product?
@@ -14,7 +15,8 @@ struct CheckOutView: View {
var body: some View { var body: some View {
ZStack(alignment: .bottom) { 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() .ignoresSafeArea()
VStack(spacing: 10) { VStack(spacing: 10) {
@@ -48,6 +50,7 @@ struct CheckOutView: View {
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } } ToolbarItem(placement: .topBarLeading) { Button("Fertig") { dismiss() } }
ToolbarItem(placement: .topBarTrailing) { TorchButton(isOn: $torchOn) }
} }
.task { units = (try? await APIClient.shared.units()) ?? [] } .task { units = (try? await APIClient.shared.units()) ?? [] }
.alert("EAN eingeben", isPresented: $manualCodeShown) { .alert("EAN eingeben", isPresented: $manualCodeShown) {

View File

@@ -44,8 +44,10 @@ struct HomeView: View {
Button { Button {
router.route = .checkout router.route = .checkout
} label: { } 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", ActionTile(title: "Auslagern", subtitle: "Barcode scannen und Menge entnehmen",
systemImage: "arrow.up.from.line") systemImage: "arrow.up.to.line")
} }
Spacer() Spacer()
} }

View File

@@ -6,6 +6,7 @@ import SwiftUI
struct ScannerView: UIViewControllerRepresentable { struct ScannerView: UIViewControllerRepresentable {
var onCode: (String) -> Void var onCode: (String) -> Void
@Binding var isPaused: Bool @Binding var isPaused: Bool
@Binding var torchOn: Bool
func makeCoordinator() -> Coordinator { Coordinator(onCode: onCode) } func makeCoordinator() -> Coordinator { Coordinator(onCode: onCode) }
@@ -17,6 +18,7 @@ struct ScannerView: UIViewControllerRepresentable {
func updateUIViewController(_ controller: ScannerViewController, context: Context) { func updateUIViewController(_ controller: ScannerViewController, context: Context) {
context.coordinator.isPaused = isPaused context.coordinator.isPaused = isPaused
controller.setTorch(on: torchOn)
} }
final class Coordinator: NSObject, ScannerViewControllerDelegate { 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 { protocol ScannerViewControllerDelegate: AnyObject {
func scanner(_ controller: ScannerViewController, didFind code: String) func scanner(_ controller: ScannerViewController, didFind code: String)
} }
@@ -48,10 +64,19 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje
private let session = AVCaptureSession() private let session = AVCaptureSession()
private var preview: AVCaptureVideoPreviewLayer? 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() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .black view.backgroundColor = .black
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(focusOnTap)))
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard granted else { return } guard granted else { return }
DispatchQueue.main.async { self?.configure() } DispatchQueue.main.async { self?.configure() }
@@ -59,16 +84,36 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje
} }
private func configure() { private func configure() {
guard let device = AVCaptureDevice.default(for: .video), let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
let input = try? AVCaptureDeviceInput(device: device), ?? AVCaptureDevice.default(for: .video)
guard let camera,
let input = try? AVCaptureDeviceInput(device: camera),
session.canAddInput(input) else { return } 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) session.addInput(input)
let output = AVCaptureMetadataOutput() let output = AVCaptureMetadataOutput()
guard session.canAddOutput(output) else { return } guard session.canAddOutput(output) else {
session.commitConfiguration()
return
}
session.addOutput(output) session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main) 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) let layer = AVCaptureVideoPreviewLayer(session: session)
layer.videoGravity = .resizeAspectFill layer.videoGravity = .resizeAspectFill
@@ -79,6 +124,52 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje
start() 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() { func start() {
guard !session.isRunning else { return } guard !session.isRunning else { return }
DispatchQueue.global(qos: .userInitiated).async { [weak self] in self?.session.startRunning() } 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 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() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()

View File

@@ -21,12 +21,15 @@ targets:
- path: Sources - path: Sources
excludes: excludes:
- Info.plist - Info.plist
# App-Icon im Icon-Composer-Format (Xcode 26).
- path: AppIcon.icon
settings: settings:
base: base:
INFOPLIST_FILE: Sources/Info.plist INFOPLIST_FILE: Sources/Info.plist
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffle.projectgood PRODUCT_BUNDLE_IDENTIFIER: com.scarriffle.projectgood
# Signing: automatisch verwaltet. Wer mit einem anderen Apple-Konto baut, # Signing: automatisch verwaltet. Wer mit einem anderen Apple-Konto baut,
# traegt hier seine Team-ID ein (Xcode: Signing & Capabilities -> Team). # traegt hier seine Team-ID ein (Xcode: Signing & Capabilities -> Team).
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
CODE_SIGN_STYLE: Automatic CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: PP34X97WS3 DEVELOPMENT_TEAM: PP34X97WS3
MARKETING_VERSION: "1.0" MARKETING_VERSION: "1.0"