150 lines
4.8 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|