Skip to content

Commit 632efa2

Browse files
fix ios qr scanner lifecycle (#101235)
* fix ios qr scanner lifecycle * fix(ios): tighten QR scanner lifecycle change --------- Co-authored-by: joshavant <[email protected]>
1 parent ceb7a4a commit 632efa2

2 files changed

Lines changed: 79 additions & 12 deletions

File tree

apps/ios/Sources/Onboarding/QRScannerView.swift

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -120,23 +120,14 @@ struct QRScannerView: UIViewControllerRepresentable {
120120
context.coordinator.reportError("Camera scanning is currently unavailable.")
121121
return UIViewController()
122122
}
123-
let scanner = DataScannerViewController(
124-
recognizedDataTypes: [.barcode(symbologies: [.qr])],
125-
isHighlightingEnabled: true)
126-
scanner.delegate = context.coordinator
127-
do {
128-
try scanner.startScanning()
129-
} catch {
130-
context.coordinator.reportError("Could not start QR scanner.")
131-
}
132-
return scanner
123+
return QRScannerContainerViewController(coordinator: context.coordinator)
133124
}
134125

135126
func updateUIViewController(_: UIViewController, context _: Context) {}
136127

137128
static func dismantleUIViewController(_ uiViewController: UIViewController, coordinator: Coordinator) {
138-
if let scanner = uiViewController as? DataScannerViewController {
139-
scanner.stopScanning()
129+
if let scanner = uiViewController as? QRScannerContainerViewController {
130+
scanner.stopScannerCapture()
140131
}
141132
coordinator.parent.onDismiss()
142133
}
@@ -145,6 +136,68 @@ struct QRScannerView: UIViewControllerRepresentable {
145136
Coordinator(parent: self)
146137
}
147138

139+
final class QRScannerContainerViewController: UIViewController {
140+
private let coordinator: Coordinator
141+
private let scanner: DataScannerViewController
142+
private var didStartScanning = false
143+
144+
init(coordinator: Coordinator) {
145+
self.coordinator = coordinator
146+
self.scanner = DataScannerViewController(
147+
recognizedDataTypes: [.barcode(symbologies: [.qr])],
148+
isHighlightingEnabled: true)
149+
super.init(nibName: nil, bundle: nil)
150+
self.scanner.delegate = coordinator
151+
}
152+
153+
@available(*, unavailable)
154+
required init?(coder _: NSCoder) {
155+
fatalError("init(coder:) has not been implemented")
156+
}
157+
158+
override func viewDidLoad() {
159+
super.viewDidLoad()
160+
self.view.backgroundColor = .systemBackground
161+
self.addChild(self.scanner)
162+
self.scanner.view.translatesAutoresizingMaskIntoConstraints = false
163+
self.view.addSubview(self.scanner.view)
164+
NSLayoutConstraint.activate([
165+
self.scanner.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
166+
self.scanner.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
167+
self.scanner.view.topAnchor.constraint(equalTo: self.view.topAnchor),
168+
self.scanner.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
169+
])
170+
self.scanner.didMove(toParent: self)
171+
}
172+
173+
override func viewDidAppear(_ animated: Bool) {
174+
super.viewDidAppear(animated)
175+
// VisionKit owns the camera session; start it only after UIKit has
176+
// presented the scanner so sheet teardown cannot race construction.
177+
self.startScanningIfNeeded()
178+
}
179+
180+
override func viewWillDisappear(_ animated: Bool) {
181+
self.stopScannerCapture()
182+
super.viewWillDisappear(animated)
183+
}
184+
185+
func stopScannerCapture() {
186+
self.scanner.stopScanning()
187+
self.didStartScanning = false
188+
}
189+
190+
private func startScanningIfNeeded() {
191+
guard !self.didStartScanning else { return }
192+
do {
193+
try self.scanner.startScanning()
194+
self.didStartScanning = true
195+
} catch {
196+
self.coordinator.reportError("Could not start QR scanner.")
197+
}
198+
}
199+
}
200+
148201
final class Coordinator: NSObject, DataScannerViewControllerDelegate {
149202
let parent: QRScannerView
150203
private var handled = false

apps/ios/Tests/RootTabsSourceGuardTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,13 +792,27 @@ struct RootTabsSourceGuardTests {
792792
settingsPendingSetupHandler.range(of: "self.scannerResultHandoff.cancel()"))
793793
let settingsSetupStaging = try #require(
794794
settingsPendingSetupHandler.range(of: "self.stagedGatewaySetupLink = link"))
795+
let scannerMake = try Self.extract(
796+
scannerSource,
797+
from: "func makeUIViewController",
798+
to: "func updateUIViewController")
799+
let scannerLifecycle = try Self.extract(
800+
scannerSource,
801+
from: "final class QRScannerContainerViewController",
802+
to: "final class Coordinator")
795803
let scannerDelivery = try Self.extract(
796804
scannerSource,
797805
from: "private func deliver(_ result: QRScannerResult",
798806
to: "func dataScanner(_: DataScannerViewController, didRemove")
799807
let stopScanning = try #require(scannerDelivery.range(of: "scanner.stopScanning()"))
800808
let deliverResult = try #require(scannerDelivery.range(of: "self.parent.onResult(result)"))
801809
#expect(scannerSource.contains("static let defaultSettlingNanoseconds: UInt64 = 1_200_000_000"))
810+
#expect(scannerSource.contains("QRScannerContainerViewController(coordinator: context.coordinator)"))
811+
#expect(!scannerMake.contains("startScanning()"))
812+
#expect(scannerLifecycle.contains("override func viewDidAppear"))
813+
#expect(scannerLifecycle.contains("try self.scanner.startScanning()"))
814+
#expect(scannerLifecycle.contains("override func viewWillDisappear"))
815+
#expect(scannerLifecycle.contains("self.stopScannerCapture()"))
802816
let activeProblemToast = try Self.extract(
803817
rootSource,
804818
from: "private var activeGatewayProblemToast: GatewayConnectionProblem?",

0 commit comments

Comments
 (0)