Files
Kecalek_python/ios_client 0.8.5/Kecalek/Views/Verification/QRCodeScannerView.swift
2026-03-14 12:43:56 +01:00

150 lines
4.8 KiB
Swift

import SwiftUI
import AVFoundation
struct QRCodeScannerView: View {
let onScan: (Data) -> Void
@Environment(\.dismiss) private var dismiss
@State private var cameraPermission: CameraPermission = .unknown
enum CameraPermission {
case unknown, granted, denied
}
var body: some View {
NavigationStack {
ZStack {
switch cameraPermission {
case .unknown:
ProgressView("Requesting camera access...")
case .denied:
VStack(spacing: 16) {
Image(systemName: "camera.fill")
.font(.system(size: 48))
.foregroundStyle(.secondary)
Text("Camera access is required to scan QR codes.")
.multilineTextAlignment(.center)
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
.buttonStyle(.borderedProminent)
}
.padding()
case .granted:
ScannerRepresentable(onScan: { data in
onScan(data)
dismiss()
})
.ignoresSafeArea()
}
}
.navigationTitle("Scan QR Code")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") { dismiss() }
}
}
}
.task {
await checkCameraPermission()
}
}
private func checkCameraPermission() async {
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized:
cameraPermission = .granted
case .notDetermined:
let granted = await AVCaptureDevice.requestAccess(for: .video)
cameraPermission = granted ? .granted : .denied
default:
cameraPermission = .denied
}
}
}
// MARK: - Scanner UIKit wrapper
private struct ScannerRepresentable: UIViewControllerRepresentable {
let onScan: (Data) -> Void
func makeUIViewController(context: Context) -> ScannerViewController {
let vc = ScannerViewController()
vc.onScan = onScan
return vc
}
func updateUIViewController(_ uiViewController: ScannerViewController, context: Context) {}
}
final class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
var onScan: ((Data) -> Void)?
private var captureSession: AVCaptureSession?
private var previewLayer: AVCaptureVideoPreviewLayer?
private var hasScanned = false
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .black
setupCamera()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
previewLayer?.frame = view.bounds
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
captureSession?.stopRunning()
}
private func setupCamera() {
let session = AVCaptureSession()
captureSession = session
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 = [.qr]
let layer = AVCaptureVideoPreviewLayer(session: session)
layer.videoGravity = .resizeAspectFill
layer.frame = view.bounds
view.layer.addSublayer(layer)
previewLayer = layer
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
}
}
func metadataOutput(_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection) {
guard !hasScanned,
let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
object.type == .qr else { return }
hasScanned = true
captureSession?.stopRunning()
// QR codes contain base64-encoded binary data (matching Python client)
if let stringValue = object.stringValue,
let data = Data(base64Encoded: stringValue) {
onScan?(data)
}
}
}