fix ios qr scanner lifecycle (#101235)

* fix ios qr scanner lifecycle

* fix(ios): tighten QR scanner lifecycle change

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
Colin Johnson 2026-07-06 22:34:44 -04:00 committed by GitHub
parent ceb7a4adba
commit 632efa2d73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 79 additions and 12 deletions

View file

@ -120,23 +120,14 @@ struct QRScannerView: UIViewControllerRepresentable {
context.coordinator.reportError("Camera scanning is currently unavailable.")
return UIViewController()
}
let scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
isHighlightingEnabled: true)
scanner.delegate = context.coordinator
do {
try scanner.startScanning()
} catch {
context.coordinator.reportError("Could not start QR scanner.")
}
return scanner
return QRScannerContainerViewController(coordinator: context.coordinator)
}
func updateUIViewController(_: UIViewController, context _: Context) {}
static func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: Coordinator) {
if let scanner = uiViewController as? DataScannerViewController {
scanner.stopScanning()
if let scanner = uiViewController as? QRScannerContainerViewController {
scanner.stopScannerCapture()
}
coordinator.parent.onDismiss()
}
@ -145,6 +136,68 @@ struct QRScannerView: UIViewControllerRepresentable {
Coordinator(parent: self)
}
final class QRScannerContainerViewController: UIViewController {
private let coordinator: Coordinator
private let scanner: DataScannerViewController
private var didStartScanning = false
init(coordinator: Coordinator) {
self.coordinator = coordinator
self.scanner = DataScannerViewController(
recognizedDataTypes: [.barcode(symbologies: [.qr])],
isHighlightingEnabled: true)
super.init(nibName: nil, bundle: nil)
self.scanner.delegate = coordinator
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .systemBackground
self.addChild(self.scanner)
self.scanner.view.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(self.scanner.view)
NSLayoutConstraint.activate([
self.scanner.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.scanner.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.scanner.view.topAnchor.constraint(equalTo: self.view.topAnchor),
self.scanner.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
])
self.scanner.didMove(toParent: self)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// VisionKit owns the camera session; start it only after UIKit has
// presented the scanner so sheet teardown cannot race construction.
self.startScanningIfNeeded()
}
override func viewWillDisappear(_ animated: Bool) {
self.stopScannerCapture()
super.viewWillDisappear(animated)
}
func stopScannerCapture() {
self.scanner.stopScanning()
self.didStartScanning = false
}
private func startScanningIfNeeded() {
guard !self.didStartScanning else { return }
do {
try self.scanner.startScanning()
self.didStartScanning = true
} catch {
self.coordinator.reportError("Could not start QR scanner.")
}
}
}
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
let parent: QRScannerView
private var handled = false

View file

@ -792,6 +792,14 @@ struct RootTabsSourceGuardTests {
settingsPendingSetupHandler.range(of: "self.scannerResultHandoff.cancel()"))
let settingsSetupStaging = try #require(
settingsPendingSetupHandler.range(of: "self.stagedGatewaySetupLink = link"))
let scannerMake = try Self.extract(
scannerSource,
from: "func makeUIViewController",
to: "func updateUIViewController")
let scannerLifecycle = try Self.extract(
scannerSource,
from: "final class QRScannerContainerViewController",
to: "final class Coordinator")
let scannerDelivery = try Self.extract(
scannerSource,
from: "private func deliver(_ result: QRScannerResult",
@ -799,6 +807,12 @@ struct RootTabsSourceGuardTests {
let stopScanning = try #require(scannerDelivery.range(of: "scanner.stopScanning()"))
let deliverResult = try #require(scannerDelivery.range(of: "self.parent.onResult(result)"))
#expect(scannerSource.contains("static let defaultSettlingNanoseconds: UInt64 = 1_200_000_000"))
#expect(scannerSource.contains("QRScannerContainerViewController(coordinator: context.coordinator)"))
#expect(!scannerMake.contains("startScanning()"))
#expect(scannerLifecycle.contains("override func viewDidAppear"))
#expect(scannerLifecycle.contains("try self.scanner.startScanning()"))
#expect(scannerLifecycle.contains("override func viewWillDisappear"))
#expect(scannerLifecycle.contains("self.stopScannerCapture()"))
let activeProblemToast = try Self.extract(
rootSource,
from: "private var activeGatewayProblemToast: GatewayConnectionProblem?",