QRScannerView.swift (5473B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Bohdan, Volodymyr Potuzhnyi 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 import SwiftUI 18 import AVFoundation 19 20 struct QRScannerView: UIViewControllerRepresentable { 21 let onResult: (Result<String, Error>) -> Void 22 23 func makeUIViewController(context: Context) -> QRScannerViewController { 24 let vc = QRScannerViewController() 25 vc.onResult = onResult 26 return vc 27 } 28 29 func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) {} 30 } 31 32 final class QRScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { 33 var onResult: ((Result<String, Error>) -> Void)? 34 private let captureSession = AVCaptureSession() 35 private var previewLayer: AVCaptureVideoPreviewLayer? 36 private var hasReported = false 37 38 override func viewDidLoad() { 39 super.viewDidLoad() 40 view.backgroundColor = .black 41 42 switch AVCaptureDevice.authorizationStatus(for: .video) { 43 case .authorized: 44 setupCamera() 45 case .notDetermined: 46 AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in 47 DispatchQueue.main.async { 48 if granted { 49 self?.setupCamera() 50 } else { 51 self?.reportError() 52 } 53 } 54 } 55 default: 56 reportError() 57 } 58 } 59 60 override func viewDidLayoutSubviews() { 61 super.viewDidLayoutSubviews() 62 previewLayer?.frame = view.bounds 63 } 64 65 override func viewWillAppear(_ animated: Bool) { 66 super.viewWillAppear(animated) 67 updatePreviewRotation() 68 NotificationCenter.default.addObserver( 69 self, 70 selector: #selector(orientationDidChange), 71 name: UIDevice.orientationDidChangeNotification, 72 object: nil 73 ) 74 if !captureSession.isRunning { 75 DispatchQueue.global(qos: .userInitiated).async { [weak self] in 76 self?.captureSession.startRunning() 77 } 78 } 79 } 80 81 override func viewWillDisappear(_ animated: Bool) { 82 super.viewWillDisappear(animated) 83 NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) 84 if captureSession.isRunning { 85 captureSession.stopRunning() 86 } 87 } 88 89 @objc private func orientationDidChange() { 90 updatePreviewRotation() 91 } 92 93 private func setupCamera() { 94 guard let device = AVCaptureDevice.default(for: .video), 95 let input = try? AVCaptureDeviceInput(device: device) else { 96 reportError() 97 return 98 } 99 100 if captureSession.canAddInput(input) { 101 captureSession.addInput(input) 102 } 103 104 let output = AVCaptureMetadataOutput() 105 if captureSession.canAddOutput(output) { 106 captureSession.addOutput(output) 107 output.setMetadataObjectsDelegate(self, queue: .main) 108 output.metadataObjectTypes = [.qr] 109 } 110 111 let layer = AVCaptureVideoPreviewLayer(session: captureSession) 112 layer.videoGravity = .resizeAspectFill 113 layer.frame = view.bounds 114 view.layer.addSublayer(layer) 115 previewLayer = layer 116 117 DispatchQueue.global(qos: .userInitiated).async { [weak self] in 118 self?.captureSession.startRunning() 119 DispatchQueue.main.async { 120 self?.updatePreviewRotation() 121 } 122 } 123 } 124 125 private func updatePreviewRotation() { 126 guard let connection = previewLayer?.connection, 127 connection.isVideoOrientationSupported else { return } 128 let orientation: AVCaptureVideoOrientation 129 switch view.window?.windowScene?.interfaceOrientation { 130 case .portrait: orientation = .portrait 131 case .landscapeLeft: orientation = .landscapeLeft 132 case .landscapeRight: orientation = .landscapeRight 133 case .portraitUpsideDown: orientation = .portraitUpsideDown 134 default: orientation = .landscapeRight 135 } 136 connection.videoOrientation = orientation 137 } 138 139 private func reportError() { 140 guard !hasReported else { return } 141 hasReported = true 142 onResult?(.failure(NSError(domain: "QRScanner", code: 1))) 143 } 144 145 func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { 146 guard !hasReported, 147 let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject, 148 let value = object.stringValue else { return } 149 hasReported = true 150 captureSession.stopRunning() 151 onResult?(.success(value)) 152 } 153 }