iOS: Scanner – Pinch-Zoom und schlaueres Blitzlicht

- Zwei-Finger-Pinch zoomt im Kamerabild naeher an einen Code heran (digital,
  auf Faktor 6 begrenzt).
- Blitz geht nach dem ersten erfolgreichen Scan wieder aus. Per Langdruck auf
  den Blitz-Knopf bleibt er dauerhaft an (gesperrt, gelb); Tippen schaltet aus
  und hebt die Sperre auf. Gilt fuer alle Scan-Bildschirme.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-26 22:42:12 +02:00
parent 944edbccd0
commit 761c131286
5 changed files with 58 additions and 9 deletions

View File

@@ -7,6 +7,9 @@ struct ScannerView: UIViewControllerRepresentable {
var onCode: (String) -> Void
@Binding var isPaused: Bool
@Binding var torchOn: Bool
/// Per Langdruck gesperrter Blitz bleibt auch nach einem Scan an. Standard:
/// aus nach dem ersten Treffer.
@Binding var torchLocked: Bool
func makeCoordinator() -> Coordinator { Coordinator(onCode: onCode) }
@@ -18,12 +21,16 @@ struct ScannerView: UIViewControllerRepresentable {
func updateUIViewController(_ controller: ScannerViewController, context: Context) {
context.coordinator.isPaused = isPaused
context.coordinator.torchLocked = torchLocked
context.coordinator.torchBinding = $torchOn
controller.setTorch(on: torchOn)
}
final class Coordinator: NSObject, ScannerViewControllerDelegate {
let onCode: (String) -> Void
var isPaused: Bool = false
var torchLocked: Bool = false
var torchBinding: Binding<Bool>?
private var lastCode: String?
private var lastTime: Date = .distantPast
@@ -37,21 +44,42 @@ struct ScannerView: UIViewControllerRepresentable {
lastTime = Date()
AudioServicesPlaySystemSound(1057)
onCode(code)
// Blitz nach dem Scan ausschalten ausser er ist per Langdruck gesperrt.
if !torchLocked, torchBinding?.wrappedValue == true {
DispatchQueue.main.async { [weak self] in self?.torchBinding?.wrappedValue = false }
}
}
}
}
/// Licht-Schalter fuer die Scan-Bildschirme.
///
/// Tippen: an/aus angeschaltet geht der Blitz nach dem naechsten Scan wieder
/// aus. Langdruck: dauerhaft an (gesperrt, gelb), bleibt auch nach Scans an;
/// erneutes Tippen schaltet aus und hebt die Sperre auf.
struct TorchButton: View {
@Binding var isOn: Bool
@Binding var locked: Bool
var body: some View {
Button {
locked = false
isOn.toggle()
} label: {
Image(systemName: isOn ? "bolt.fill" : "bolt.slash.fill")
.foregroundStyle(locked ? Color.yellow : Color.accentColor)
}
.accessibilityLabel(isOn ? "Licht ausschalten" : "Licht einschalten")
.simultaneousGesture(
LongPressGesture(minimumDuration: 0.4).onEnded { _ in
isOn = true
locked = true
}
)
.accessibilityLabel(
isOn
? (locked ? "Licht dauerhaft an, zum Ausschalten tippen" : "Licht aus")
: "Licht ein (bleibt bis zum nächsten Scan)"
)
}
}
@@ -65,6 +93,8 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje
private let session = AVCaptureSession()
private var preview: AVCaptureVideoPreviewLayer?
private var device: AVCaptureDevice?
/// Zoomfaktor beim Beginn einer Pinch-Geste (Ausgangspunkt fuers Skalieren).
private var zoomBasis: CGFloat = 1.0
/// Auch krummere Symbologien mitnehmen: ITF-14 kommt auf Umkartons vor,
/// DataMatrix auf kleinen Aufklebern.
@@ -77,6 +107,7 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje
super.viewDidLoad()
view.backgroundColor = .black
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(focusOnTap)))
view.addGestureRecognizer(UIPinchGestureRecognizer(target: self, action: #selector(zoomPinch)))
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard granted else { return }
DispatchQueue.main.async { self?.configure() }
@@ -164,6 +195,20 @@ final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObje
device.unlockForConfiguration()
}
/// Zwei-Finger-Pinch zoomt naeher an einen Code heran (rein digital, auf ein
/// sinnvolles Maximum begrenzt zu viel Zoom macht den Code unscharf).
@objc private func zoomPinch(_ gesture: UIPinchGestureRecognizer) {
guard let device else { return }
if gesture.state == .began {
zoomBasis = device.videoZoomFactor
}
let maxZoom = min(device.activeFormat.videoMaxZoomFactor, 6.0)
let neu = max(1.0, min(zoomBasis * gesture.scale, maxZoom))
guard (try? device.lockForConfiguration()) != nil else { return }
device.videoZoomFactor = neu
device.unlockForConfiguration()
}
/// Kurzer gelber Fokus-Rahmen an der getippten Stelle als Rueckmeldung.
private func showFocusIndicator(at point: CGPoint) {
let seite: CGFloat = 72