a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
104 lines
3.1 KiB
Swift
104 lines
3.1 KiB
Swift
import AVFoundation
|
|
import SwiftUI
|
|
|
|
/// Camera-based QR scanner for pairing codes.
|
|
struct QRScannerView: UIViewControllerRepresentable {
|
|
let onScan: (String) -> Void
|
|
|
|
func makeUIViewController(context: Context) -> ScannerViewController {
|
|
let controller = ScannerViewController()
|
|
controller.onScan = onScan
|
|
return controller
|
|
}
|
|
|
|
func updateUIViewController(_ controller: ScannerViewController, context: Context) {}
|
|
}
|
|
|
|
final class ScannerViewController: UIViewController, @preconcurrency AVCaptureMetadataOutputObjectsDelegate {
|
|
var onScan: ((String) -> Void)?
|
|
|
|
private let session = AVCaptureSession()
|
|
private var previewLayer: AVCaptureVideoPreviewLayer?
|
|
private var didScan = false
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
view.backgroundColor = .black
|
|
|
|
guard let device = AVCaptureDevice.default(for: .video),
|
|
let input = try? AVCaptureDeviceInput(device: device),
|
|
session.canAddInput(input)
|
|
else {
|
|
showUnavailableLabel()
|
|
return
|
|
}
|
|
session.addInput(input)
|
|
|
|
let output = AVCaptureMetadataOutput()
|
|
guard session.canAddOutput(output) else {
|
|
showUnavailableLabel()
|
|
return
|
|
}
|
|
session.addOutput(output)
|
|
output.setMetadataObjectsDelegate(self, queue: .main)
|
|
output.metadataObjectTypes = [.qr]
|
|
|
|
let preview = AVCaptureVideoPreviewLayer(session: session)
|
|
preview.videoGravity = .resizeAspectFill
|
|
preview.frame = view.bounds
|
|
view.layer.addSublayer(preview)
|
|
previewLayer = preview
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
previewLayer?.frame = view.bounds
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
didScan = false
|
|
let session = session
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
if !session.isRunning {
|
|
session.startRunning()
|
|
}
|
|
}
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
let session = session
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
if session.isRunning {
|
|
session.stopRunning()
|
|
}
|
|
}
|
|
}
|
|
|
|
func metadataOutput(
|
|
_ output: AVCaptureMetadataOutput,
|
|
didOutput metadataObjects: [AVMetadataObject],
|
|
from connection: AVCaptureConnection
|
|
) {
|
|
guard !didScan,
|
|
let object = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
|
|
object.type == .qr,
|
|
let value = object.stringValue
|
|
else { return }
|
|
didScan = true
|
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
|
onScan?(value)
|
|
}
|
|
|
|
private func showUnavailableLabel() {
|
|
let label = UILabel()
|
|
label.text = "Camera unavailable"
|
|
label.textColor = .white
|
|
label.textAlignment = .center
|
|
label.frame = view.bounds
|
|
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
view.addSubview(label)
|
|
}
|
|
}
|