chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user