iOS pairing scanner: surface "could not start" + Try Again
On-device logs proved the camera is being blocked by device state, not the app: every retry showed window=true app=active scene=foregroundActive yet reason-1 (videoDeviceNotAvailableInBackground) persisted for 6s — the signature of iPhone Mirroring / a locked tethered device, where iOS disables the camera (the recurring com.apple.PointerUI log is the Mac pointer driving the phone). Rather than sit on a black screen after retries are exhausted, report a new .couldNotStart state explaining the likely cause (locked / mirrored) with a "Try Again" button that rebuilds the capture session from scratch (via .id(retryToken)). App-side capture code is correct; this is a graceful fallback for an environment that withholds the camera. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
765183d25b
commit
cfa823bc13
@@ -12,6 +12,8 @@ enum ScannerCameraState: Equatable {
|
|||||||
case scanning // feed is live
|
case scanning // feed is live
|
||||||
case denied // permission off — needs Settings
|
case denied // permission off — needs Settings
|
||||||
case unavailable // no camera on this device (e.g. the Simulator)
|
case unavailable // no camera on this device (e.g. the Simulator)
|
||||||
|
case couldNotStart // camera exists + authorized but the session won't run
|
||||||
|
// (iPhone locked / mirrored to a Mac / in use elsewhere)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Camera QR scanner (UX_IOS §7). Decodes the Mac's `nucleic://pair?d=…` QR into a
|
/// Camera QR scanner (UX_IOS §7). Decodes the Mac's `nucleic://pair?d=…` QR into a
|
||||||
@@ -21,12 +23,16 @@ struct PairingScannerView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
@State private var cameraState: ScannerCameraState = .scanning
|
@State private var cameraState: ScannerCameraState = .scanning
|
||||||
|
/// Bumping this rebuilds the scanner controller from scratch (fresh session) — the "Try
|
||||||
|
/// Again" path after the camera couldn't start.
|
||||||
|
@State private var retryToken = 0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.ignoresSafeArea()
|
Color.black.ignoresSafeArea()
|
||||||
QRScannerRepresentable(onFound: handle, onState: { cameraState = $0 })
|
QRScannerRepresentable(onFound: handle, onState: { cameraState = $0 })
|
||||||
|
.id(retryToken)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
VStack {
|
VStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -70,6 +76,13 @@ struct PairingScannerView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
|
case .couldNotStart:
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
banner("Couldn't start the camera. If your iPhone is locked or being mirrored to a Mac, the camera is blocked — open Nucleic directly on your unlocked iPhone, then try again.",
|
||||||
|
tint: .black.opacity(0.6))
|
||||||
|
Button("Try Again") { cameraState = .scanning; retryToken += 1 }
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +272,9 @@ final class QRScannerController: UIViewController, AVCaptureMetadataOutputObject
|
|||||||
// is foreground; back off briefly and retry until it sticks (bounded).
|
// is foreground; back off briefly and retry until it sticks (bounded).
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self, self.startAttempts < 12 else {
|
guard let self, self.startAttempts < 12 else {
|
||||||
scannerLog.error("📷 gave up starting after retries"); return
|
scannerLog.error("📷 gave up starting after retries")
|
||||||
|
self?.report(.couldNotStart)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
self.startAttempts += 1
|
self.startAttempts += 1
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.startSessionIfReady() }
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in self?.startSessionIfReady() }
|
||||||
|
|||||||
Reference in New Issue
Block a user