Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/apps/mobile/ios/OpenBitFun/Features/Pairing/PairingSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,19 @@ struct PairingSheet: View {
}
}
.fullScreenCover(isPresented: $scannerOpen) {
QRCodeScannerView { code in
pairingURL = code
scannerOpen = false
if PairingLinkHintsKt.inspectPairingLink(url: code).requiresAccount {
manualOpen = true
focused = true
} else {
model.submitPairing(url: code)
}
}
QRCodeScannerView(
onCode: { code in
pairingURL = code
scannerOpen = false
if PairingLinkHintsKt.inspectPairingLink(url: code).requiresAccount {
manualOpen = true
focused = true
} else {
model.submitPairing(url: code)
}
},
onCancel: { scannerOpen = false }
)
.ignoresSafeArea()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ import UIKit

struct QRCodeScannerView: UIViewControllerRepresentable {
let onCode: (String) -> Void
let onCancel: () -> Void

func makeUIViewController(context: Context) -> QRScannerController {
let controller = QRScannerController()
controller.onCode = onCode
controller.onCancel = onCancel
return controller
}

func updateUIViewController(_ uiViewController: QRScannerController, context: Context) {}
}

final class QRScannerController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
private let session = AVCaptureSession()
private let sessionQueue = DispatchQueue(label: "com.openbitfun.mobile.ios.qr-session")
private lazy var session = AVCaptureSession()
private var previewLayer: AVCaptureVideoPreviewLayer?
private var captureConfigured = false
private var emittedCode = false
var onCode: ((String) -> Void)?
var onCancel: (() -> Void)?

override func viewDidLoad() {
super.viewDidLoad()
Expand All @@ -27,7 +33,10 @@ final class QRScannerController: UIViewController, AVCaptureMetadataOutputObject
close.tintColor = UIColor(OpenBitFunTheme.contentOnAction)
close.backgroundColor = UIColor(OpenBitFunTheme.mediaControlBackground)
close.layer.cornerRadius = 22
close.addAction(UIAction { [weak self] _ in self?.dismiss(animated: true) }, for: .touchUpInside)
close.addAction(UIAction { [weak self] _ in
self?.stopCapture()
self?.onCancel?()
}, for: .touchUpInside)
close.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(close)
NSLayoutConstraint.activate([
Expand All @@ -37,44 +46,81 @@ final class QRScannerController: UIViewController, AVCaptureMetadataOutputObject
close.heightAnchor.constraint(equalToConstant: 44),
])

guard AVCaptureDevice.authorizationStatus(for: .video) != .denied else { return }
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard granted else { return }
DispatchQueue.main.async { self?.configureCapture() }
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
configureCaptureAsync()
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
guard granted else { return }
self?.configureCaptureAsync()
}
default:
break
}
}

override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopCapture()
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
previewLayer?.frame = view.bounds
}

private func configureCaptureAsync() {
sessionQueue.async { [weak self] in
self?.configureCapture()
}
}

private func configureCapture() {
guard !captureConfigured else { return }
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { return }
let output = AVCaptureMetadataOutput()
guard session.canAddOutput(output) else { return }
session.beginConfiguration()
session.addInput(input)
session.addOutput(output)
output.setMetadataObjectsDelegate(self, queue: .main)
output.setMetadataObjectsDelegate(self, queue: sessionQueue)
output.metadataObjectTypes = [.qr]
let layer = AVCaptureVideoPreviewLayer(session: session)
layer.videoGravity = .resizeAspectFill
view.layer.insertSublayer(layer, at: 0)
previewLayer = layer
session.commitConfiguration()
captureConfigured = true
DispatchQueue.main.async { [weak self] in
guard let self, self.previewLayer == nil else { return }
let layer = AVCaptureVideoPreviewLayer(session: self.session)
layer.videoGravity = .resizeAspectFill
layer.frame = self.view.bounds
self.view.layer.insertSublayer(layer, at: 0)
self.previewLayer = layer
}
session.startRunning()
}

private func stopCapture() {
sessionQueue.async { [weak self] in
guard let self, self.session.isRunning else { return }
self.session.stopRunning()
}
}

func metadataOutput(
_ output: AVCaptureMetadataOutput,
didOutput metadataObjects: [AVMetadataObject],
from connection: AVCaptureConnection,
) {
guard let value = (metadataObjects.first as? AVMetadataMachineReadableCodeObject)?.stringValue,
!value.isEmpty else { return }
session.stopRunning()
onCode?(value)
dismiss(animated: true)
!value.isEmpty,
!emittedCode else { return }
emittedCode = true
if session.isRunning {
session.stopRunning()
}
DispatchQueue.main.async { [weak self] in
self?.onCode?(value)
}
}
}