Skip to content

wip(ios): adopt the UIScene lifecycle so the app can launch on iOS 26+ - #11570

Draft
formed2forge wants to merge 6 commits into
BasedHardware:mainfrom
formed2forge:fix/ios-uiscene-lifecycle
Draft

wip(ios): adopt the UIScene lifecycle so the app can launch on iOS 26+#11570
formed2forge wants to merge 6 commits into
BasedHardware:mainfrom
formed2forge:fix/ios-uiscene-lifecycle

Conversation

@formed2forge

@formed2forge formed2forge commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Draft, updated. All 6 commits are now on this branch, including a correction to a false
premise in commit 1 (see commit 6 and the correction note below). didInitializeImplicitFlutterEngine
— where all the migrated plugin/channel registration lives — is now confirmed to actually fire
on hardware, which it was not before commit 6. Still draft: the 11 migrated channels, swipe-kill,
and the foreground BLE-reconnect path are only proven to register/fire, not exercised
end-to-end. See "What's left."

Why

Apps built against the iOS 26+ SDK must adopt the UIScene lifecycle. Omi's app/ios/Runner/Info.plist had no UIApplicationSceneManifest, so UIKit trapped during scene setup before the Dart VM started — the app installed and signed fine, showed the launch storyboard, and stayed there permanently with no crash dialog, no UI, and no Dart code executed. Full diagnosis in #11568.

iOS 27 ships publicly in about two months, so this is a deadline rather than a hypothetical.

Correction — commit 1's premise was wrong, and it hid a real bug for hours

Commit 1 omitted UISceneStoryboardFile from the manifest because "this project has no Main.storyboard." That was never true. app/ios/Runner/Base.lproj/Main.storyboard exists, is a completely standard stock FlutterViewController scene, and is correctly wired into Runner.xcodeproj's resource build phases for both the prod and dev targets. The false claim traces to running ls ios/Runner/*.storyboard, which doesn't recurse into the .lproj subdirectory Xcode puts localized storyboards in — an easy, mechanical mistake, and one worth flagging so it isn't repeated: prefer find ios/Runner -iname '*.storyboard' when checking whether an Xcode resource exists.

Consequence: without UISceneStoryboardFile, UIKit never auto-instantiates a root view controller when the scene connects, so no FlutterViewController is ever created — and since the implicit Flutter engine is created lazily when that happens, didInitializeImplicitFlutterEngine (where commit 3's entire plugin/channel migration lives) never fired. The result was a silent blank screen, not a crash.

This went undetected through the "MILESTONE" verification recorded against commits 1-3 (process alive N seconds after launch, no termination error) because that testing used devicectl device process launch directly, and debug-mode Flutter builds refuse to create an engine at all outside Xcode or flutter run ("Cannot create a FlutterEngine instance in debug mode without Flutter tooling or Xcode... profile and release mode apps can be launched from the home screen"). So the app never even attempted engine creation under that test — "process alive, no crash" was true, but proved nothing about whether the app was rendering anything. It happened to look identical to success.

Found and fixed by rebuilding --profile instead of --debug (profile mode can launch from a bare devicectl launch, unlike debug), which surfaced the real state: blank screen, confirmed via a temporary log probe that didInitializeImplicitFlutterEngine was not firing. Restoring UISceneStoryboardFile = Main fixed it — the same probe now fires. The app then reaches an unrelated crash (see below), which is itself further proof of forward progress: it wasn't reachable before this fix.

What this PR does now (6 commits)

  1. 05b3312416 — adds UIApplicationSceneManifest to Info.plist. Clears the native launch trap: the Dart VM Service becomes discoverable and DartWorker/AudioSession threads come up. The app then SIGABRTs, because window?.rootViewController is nil under scenes and AppDelegate.swift force-unwraps it in nine places. Its UISceneStoryboardFile omission was later found to be wrong — see commit 6.
  2. 69b1d80958 — carries the same manifest into the tracked Info-Dev.plist, which the dev/raybanDat xcconfigs actually build against. Without this, a plain Xcode dev build used the stale tracked copy and still crashed.
  3. 4e766a7b13 — the AppDelegate migration itself. AppDelegate now conforms to FlutterImplicitEngineDelegate; every plugin-registration and binaryMessenger consumer (GeneratedPluginRegistrant, OmiPhoneCallsPlugin, the Watch/BLE/Ray-Ban/phone-mic Pigeon APIs, and all seven method channels plus WifiNetworkPlugin) moved into didInitializeImplicitFlutterEngine(_:), sourcing the registry from engineBridge.pluginRegistry and the messenger from engineBridge.applicationRegistrar.messenger(). No window?.rootViewController reference remains and all nine force unwraps are gone. Incidental fix bundled in: a launch carrying a deep link previously hit an early return true in didFinishLaunchingWithOptions and skipped all channel setup — registration no longer shares a code path with link handling.
  4. 2fe5d7e43aapplicationWillEnterForeground never fires under the UIScene lifecycle (measured 0/4 across two full background→reopen→swipe-kill cycles on iPhone 17 Pro / iOS 27.0), so the OmiBleManager.shared.reconnectStalePeripherals() call inside it was silently dead — no crash, no log, BLE peripherals just never reconnect on foreground. Relocated the call to a NotificationCenter observer on UIApplication.willEnterForegroundNotification, which fired 4/4 in the same measurement, registered in didInitializeImplicitFlutterEngine (not in didFinishLaunchingWithOptions, for the same deep-link-early-return reason as above). applicationWillTerminate was left alone — measured 2/2, already correct, so notifyOnKill and disconnectAllPeripherals() still work.
  5. 019861ddfc — removes the dangling UIMainStoryboardFile = Main key from both plists (the old, pre-scene equivalent of UISceneStoryboardFile — also dangling because window?.rootViewController was populated by FlutterAppDelegate's own code path, not storyboard auto-instantiation, under the old lifecycle).
  6. 239c357 — restores UISceneStoryboardFile = Main, correcting commit 1. See above.

Verification

Compiling proves nothing here — the whole failure class is runtime, which is why hardware verification is load-bearing for this PR specifically. Two different launch mechanisms were used and they are not equally trustworthy:

Mechanism What it can prove What it can't
flutter run (lldb-attached) Full Dart/engine correctness — this is how the original SIGABRT fix (commits 1-3) was measured (Dart VM Service discovered, DartWorker/AudioSession threads live, first frame reached) Currently broken on this machine for an unrelated reason (Xcode/LLDB debug-symbol issue), so unavailable for commits 4-6
Bare devicectl device process launch, debug build Process-level survival only Nothing about the Flutter engine — debug builds refuse to create one outside Xcode/Flutter tooling, so "no crash" is not evidence the app is doing anything
Bare devicectl device process launch, profile build Real engine creation and app logic, since profile builds can launch this way Still not identical to a release build; good enough for this bug class

Given the middle row, the "alive N seconds, no termination error" evidence recorded for commits 3-5 during today's earlier debug-build testing should be read as inconclusive, not confirming — it's consistent with both "works" and "silently never started," and turned out to be the latter until commit 6. The profile-mode test after commit 6 is the first devicectl-based test in this PR that actually proves engine-level behavior:

Before commit 6 After
didFinishLaunchingWithOptions probe Fires Fires (unchanged)
didInitializeImplicitFlutterEngine probe Never fires Fires
Screen state Blank (silently) Progresses past engine init
Outcome No crash, no UI, no error — looked identical to a working launch under debug-mode devicectl testing Crashes on an invalid placeholder GOOGLE_APP_ID in the dev flavor's local GoogleService-Info.plist — an untracked, pre-existing community-build config placeholder, unrelated to this PR

The flutter run-based measurements for commits 1-3 (SIGABRT fix, first frame reached) remain trustworthy — that mechanism doesn't have the debug-mode gap described above.

Answers to the original open questions

  1. How should the WCSession/Watch bridge obtain a messenger without window?.rootViewController? engineBridge.applicationRegistrar.messenger() — confirmed against the Flutter 3.44.9 engine headers and now in commit 3. No FlutterViewController is needed anywhere in this file.
  2. Is UIApplicationSupportsMultipleScenes = false the intended posture? Kept off; nothing in this migration required enabling it, and FlutterSceneLifeCycleEngineRegistration documents that manual scene/engine registration is only needed when multi-scene is on.
  3. Should the dangling UIMainStoryboardFile = Main be removed? Yes — done in commit 5, but this is now superseded context: that key was the old (pre-scene) equivalent of UISceneStoryboardFile, and unlike the scene one, it really was safe to drop — FlutterAppDelegate's own pre-scene code populated window/rootViewController programmatically, so nothing depended on it. The new scene-based key needed the opposite treatment — see commit 6.

What's left before this is ready to merge

Everything below needs physical interaction with a device and has not been done from this side yet. It also needs a real (non-placeholder) Firebase config for the dev flavor to get past the crash described above — out of scope for this PR, tracked separately as a community-build config gap:

  • Exercise each of the 11 migrated channels at least once: Watch, BLE, Ray-Ban, phone-mic, reminders, health, speech, environment, audio session, wifi, battery widget. So far they are only proven to register without crashing (via flutter run, for the reasons above).
  • Swipe-to-kill: confirm the notifyOnKill notification still arrives and BLE peripherals disconnect.
  • Background → foreground: confirm reconnectStalePeripherals() actually runs via the new observer (the notification firing was measured with a temporary probe, not through this exact code path, and not via a debug-build devicectl launch for the reasons above).
  • Battery widget still updates after a real device reports battery.
  • Ideally, re-run the full channel exercise via flutter run once the local Xcode/LLDB debug-symbol issue is resolved, since that's the only mechanism proven trustworthy for this bug class.

Failure-Class: none

formed2forge added a commit to formed2forge/omi that referenced this pull request Aug 14, 2026
Info.plist and Info-Dev.plist both set UIMainStoryboardFile = Main, but
there is no Main.storyboard in ios/Runner/ — only devLaunchScreen.storyboard
and prodLaunchScreen.storyboard (the built bundle contains only those two
.storyboardc). The app-delegate lifecycle tolerated the dangling reference;
UISceneStoryboardFile could not be set to the same nonexistent name without
aborting at launch (see the manifest added in 05b3312), which is what
surfaced this as worth fixing rather than leaving as a latent oddity.

Removed from both plists with PlistBuddy; plutil -lint passes on each.
Answers open question 3 on BasedHardware#11570.

Failure-Class: none

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@formed2forge formed2forge changed the title wip(ios): declare a UIScene manifest so the app can launch on iOS 26+ wip(ios): adopt the UIScene lifecycle so the app can launch on iOS 26+ Aug 14, 2026

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @formed2forge — this is a well-documented attack on a real launch-blocker (#11568), and the migration direction matches both Apple's iOS 26 SDK requirement and Flutter's official UIScene migration template. Static review notes below; nothing here is a merge blocker yet since this is draft and device-gated.

AppDelegate.swift

  • Verified all ten base-version force-unwraps rooted in window?.rootViewController (e.g. the old controller!.binaryMessenger in the WCSession block and the unconditional let controller = window?.rootViewController ... ! chain) are gone; every channel now sources its messenger from engineBridge.applicationRegistrar.messenger(), and OmiPhoneCallsPlugin.register now uses registry.registrar(forPlugin:) instead of self.registrar(forPlugin:). That removes the launch-time crash class under scenes, not just the symptoms.
  • Good call anchoring foreground reconnect to UIApplication.willEnterForegroundNotification instead of a scene delegate, with the 0/4 vs 4/4 measurements noted — that also survives any future multi-scene flip.
  • One carried-over gap: in didFinishLaunchingWithOptions the deep-link branch still returns true before SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback and the UNUserNotificationCenter delegate assignment. The PR description calls out fixing the old early-return-skips-registration bug, but a cold launch that carries a link (e.g. a Ray-Ban Meta AppLink) still skips the foreground-task callback whose own comment says "Without this code the task will not work". Suggest moving the AppLinks check after those two setup steps (registration no longer shares that path anyway) or handling the link post-setup.
  • Minor: the BLE / Ray-Ban / phone-mic blocks now use do { ... } with no catch — that's just a scoping block in Swift, but it reads like error handling. Consider dropping the wrapper or keeping a comment.
  • For the hardware checklist: under the scene lifecycle iOS normally routes URL opens to scene(_:openURLContexts:) rather than application(_:open:options:), which is where the Ray-Ban registration callback is handled. Worth explicitly testing warm and cold Ray-Ban callback flows on device before un-drafting.

Info.plist / Info-Dev.plist

  • Manifest structure matches Flutter's documented template (UIWindowSceneSessionRoleApplication, configuration name flutter, FlutterSceneDelegate, UIApplicationSupportsMultipleScenes=false), and removing UIMainStoryboardFile is consistent with it — project.pbxproj references no Main.storyboard, so that key was stale anyway. Carrying the identical change into Info-Dev.plist (what the dev xcconfigs actually build against) avoids the stale-tracked-copy crash; good catch.

CI note: mobile-app-checks is Android/Dart-only, so nothing in CI exercises this change — device validation stays the real gate, which I know is already on your radar per "What's left". Labeling needs-maintainer-review for that reason (a maintainer with iOS hardware needs to sign off on launch/deep-link/foreground-task behavior before this leaves draft), not as a knock on the work.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@formed2forge

Copy link
Copy Markdown
Contributor Author

Two companion fixes found while verifying this migration on-device, both needed to actually see the app work end to end:

Combining all three, the app reaches the sign-in screen on iPhone 17 Pro / iOS 27.0 — confirmed on real hardware, not just a live process.

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @formed2forge — reviewed the new head (239c3570) on top of the earlier pass. Three updates, each verified against the repo:

Restored UISceneStoryboardFile — verified, and a correction to my own earlier note. app/ios/Runner/Base.lproj/Main.storyboard does exist (a stock FlutterViewController initial scene), and it is wired into Runner.xcodeproj's resource build phases for both the prod and dev targets — so my previous statement that "project.pbxproj references no Main.storyboard" was wrong, and restoring the key in both Info.plist and Info-Dev.plist is the correct manifest. The post-mortem on why "process alive, no crash" was a false signal for debug-mode engine creation is worth keeping in the description for the next person who tries to validate this way.

Companion PRs landed on main, but this branch doesn't contain them yet. #11594 and #11595 are both merged, and your comment correctly identifies the flutter_contacts 2.x migration as a merge prerequisite for this PR. As of this head, app/pubspec.yaml on the branch still pins flutter_contacts: ^1.1.9+2 (main has ^2.3.1) and the branch is several commits behind main — so a fresh checkout of this branch alone still hits the 1.x registration crash you diagnosed in #11595. Merging main into this branch before the final device-validation pass ("What's left") will make the branch self-contained and let CI reflect the prerequisite.

Still open from the last pass (unchanged, non-blocking while draft):

  • AppDelegate.swiftdidFinishLaunchingWithOptions: the AppLinks.shared.getLink branch still returns true before SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback and the UNUserNotificationCenter delegate assignment, so a cold launch carrying a deep link still skips the foreground-task callback whose own comment says "Without this code the task will not work."
  • The three do { ... } scoping blocks (BLE / Ray-Ban / phone-mic registration inside didInitializeImplicitFlutterEngine) still read like error handling without a catch; a plain block or a comment would be clearer.
  • Device checklist still needs the scene-lifecycle URL-open routing check (scene(_:openURLContexts:) vs application(_:open:options:)) for the Ray-Ban Meta callback flow, warm and cold.

The core migration remains sound on static review: all thirteen channel/Pigeon/plugin registration sites (watch recorder, BLE, Ray-Ban, phone-mic, notifyOnKill, reminders, health, speech, environment, audioSession, WiFi, battery widget, phone-calls) now source their messenger from engineBridge.applicationRegistrar.messenger(), OmiPhoneCallsPlugin registers via registry.registrar(forPlugin:), and the foreground BLE reconnect is anchored to UIApplication.willEnterForegroundNotification — which also survives a future multi-scene flip.

Keeping needs-maintainer-review: launch, deep-link, and foreground-task behavior under the scene lifecycle can only be confirmed on iOS hardware, which no CI job here covers.


Automated review by glm-5.3 acting for the maintainers. Human sign-off needed from a maintainer with iOS 26+ hardware to validate launch, deep-link, and foreground-task flows on device before this leaves draft.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@formed2forge
formed2forge force-pushed the fix/ios-uiscene-lifecycle branch from 239c357 to 862fe6e Compare August 18, 2026 02:48
formed2forge added a commit to formed2forge/omi that referenced this pull request Aug 18, 2026
Info.plist and Info-Dev.plist both set UIMainStoryboardFile = Main, but
there is no Main.storyboard in ios/Runner/ — only devLaunchScreen.storyboard
and prodLaunchScreen.storyboard (the built bundle contains only those two
.storyboardc). The app-delegate lifecycle tolerated the dangling reference;
UISceneStoryboardFile could not be set to the same nonexistent name without
aborting at launch (see the manifest added in 05b3312), which is what
surfaced this as worth fixing rather than leaving as a latent oddity.

Removed from both plists with PlistBuddy; plutil -lint passes on each.
Answers open question 3 on BasedHardware#11570.

Failure-Class: none

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks @formed2forge — quick pass over the rebased head (862fe6e6) relative to the prior automated review:

The rebase resolves the prerequisite flagged last round — verified. The branch now sits on current main and app/pubspec.yaml at the head pins flutter_contacts: ^2.3.1 (matching main), so the 1.x registration crash from #11595 no longer applies to a fresh checkout of this branch alone. It's self-contained now, as hoped.

The PR's own changes are byte-identical to the last-reviewed state. I diffed all three files (AppDelegate.swift, Info.plist, Info-Dev.plist) against the previously reviewed head — no content change, so the earlier static analysis carries over in full: thirteen registration sites sourcing their messenger from engineBridge.applicationRegistrar.messenger() inside didInitializeImplicitFlutterEngine, OmiPhoneCallsPlugin via registry.registrar(forPlugin:), foreground BLE reconnect anchored to UIApplication.willEnterForegroundNotification, and the manifest matching Flutter's template with UISceneStoryboardFile restored (re-verified against Runner.xcodeproj's resource phases).

Still open (unchanged, non-blocking while draft):

  • AppDelegate.swiftdidFinishLaunchingWithOptions: the AppLinks.shared.getLink branch still returns true before SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback and the UNUserNotificationCenter delegate assignment, so a cold launch carrying a deep link still skips the foreground-task callback whose own comment says "Without this code the task will not work."
  • The three do { ... } scoping blocks (BLE / Ray-Ban / phone-mic registration inside didInitializeImplicitFlutterEngine) still read like error handling with no catch; a plain block or a one-line comment would be clearer.
  • Device checklist still needs the scene-lifecycle URL-open routing check (scene(_:openURLContexts:) vs application(_:open:options:)) for the Ray-Ban Meta callback flow, warm and cold.

Nothing new blocking on static review. The remaining gate before un-drafting is still on-hardware validation of launch, deep-link routing, swipe-kill, and foreground-task behavior under the scene lifecycle.


by AI on behalf of David — the remaining gate is sign-off from a maintainer with iOS 26+ hardware; please @Git-on-my-level if David is needed.

cursor Bot pushed a commit to formed2forge/omi that referenced this pull request Sep 2, 2026
Info.plist and Info-Dev.plist both set UIMainStoryboardFile = Main, but
there is no Main.storyboard in ios/Runner/ — only devLaunchScreen.storyboard
and prodLaunchScreen.storyboard (the built bundle contains only those two
.storyboardc). The app-delegate lifecycle tolerated the dangling reference;
UISceneStoryboardFile could not be set to the same nonexistent name without
aborting at launch (see the manifest added in 05b3312), which is what
surfaced this as worth fixing rather than leaving as a latent oddity.

Removed from both plists with PlistBuddy; plutil -lint passes on each.
Answers open question 3 on BasedHardware#11570.

Failure-Class: none

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cursor
cursor Bot force-pushed the fix/ios-uiscene-lifecycle branch from 862fe6e to c6b94d3 Compare September 2, 2026 03:11

@Git-on-my-level Git-on-my-level left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @formed2forge — reviewed the rebased head (c6b94d3) against the previous pass (862fe6e6) and current main.

The rebase is clean — verified. The PR's own changes are unchanged in substance: same FlutterImplicitEngineDelegate conformance with plugin registration moved into didInitializeImplicitFlutterEngine(_:), same messenger sourcing across the channel setups, same willEnterForegroundNotification anchor for BLE reconnect, and both Info.plist and Info-Dev.plist carry the identical scene manifest with UISceneStoryboardFile restored. Everything else new in the head vs the last review (BGTaskScheduler cancels, the NSAllowsArbitraryLoads dev-plist comment, dictation punctuation hints, telemetry marks) came in from main via the rebase, not from this branch. Also verified UIMainStoryboardFile existed in exactly these two plists repo-wide, so dropping it is complete.

New from the rebase — the #11892 background-energy telemetry will go silent under this PR. The head now inherits applicationDidEnterBackground / applicationDidBecomeActive overrides from main (#11892, OmiBleManager.markBackgroundTelemetryStart/End). Those are app-delegate UI-state callbacks — the same class as applicationWillEnterForeground, which you measured at 0/4 under the scene lifecycle. UIKit stops calling them once this manifest ships, so the telemetry landed on Aug 20 would silently stop collecting the moment this merges. The fix is the same shape you already applied for reconnect: anchor to UIApplication.didEnterBackgroundNotification / didBecomeActiveNotification (your 4/4 result for the app-level willEnterForegroundNotification suggests the notification family keeps firing — worth confirming on device). Folding that in before the hardware pass means the validation covers it.

Sharpened from last round — the Ray-Ban URL callback looks statically dead. rayBanMetaHostApi?.handleUrl(url) lives in application(_:open:options:), another callback the scene delegate takes over; on scene-lifecycle apps URL opens route to scene(_:openURLContexts:) instead. Unless routing is added scene-side, the DAT-camera callback flow never fires. This was on the device checklist; statically it now reads as a definite gap rather than a to-verify.

Still open, unchanged from the prior review: the AppLinks.shared.getLink early return true in didFinishLaunchingWithOptions still skips SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback and the UNUserNotificationCenter delegate assignment on a deep-link cold launch, and the three do { … } scoping blocks still read like error handling with no catch.

One note for maintainers: nothing in CI compiles app/ios/Runner/AppDelegate.swift (the Swift checks in this repo cover desktop only), so hardware runs remain the only compile + runtime validation for this migration.


by AI on behalf of David — the remaining gate is sign-off from a maintainer with iOS 26+ hardware, specifically to confirm the #11892 telemetry callbacks and Ray-Ban URL routing under the scene lifecycle before this leaves draft.

formed2forge and others added 6 commits September 2, 2026 15:31
Partial, deliberately incomplete — see BasedHardware#11568. Opening as a draft because the
remaining half is an AppDelegate migration that needs a decision on approach
before it is written.

Apps built against the iOS 26+ SDK must adopt the UIScene lifecycle. Without
UIApplicationSceneManifest, UIKit traps during scene setup and the Dart VM never
starts, so the app sits on the launch storyboard forever with no crash, no UI,
and no Dart code executed at all.

This commit adds only the manifest. Measured effect on iPhone 17 Pro / iOS 27.0
with Flutter 3.44.9:

  before: "The Dart VM Service was not discovered after 60 seconds",
          EXC_BREAKPOINT in UIKitCore via FrontBoardServices, signal 5
  after:  Dart VM Service discovered (http://127.0.0.1:50817/...),
          DartWorker and AudioSession threads live, then SIGABRT

So the manifest clears the native launch trap but is not sufficient on its own.
UISceneStoryboardFile is intentionally omitted: Flutter's template sets it to
"Main" and this project has no Main.storyboard, only devLaunchScreen and
prodLaunchScreen, and pointing a scene at a missing storyboard aborts.

Still to do, tracked in BasedHardware#11568: AppDelegate must conform to
FlutterImplicitEngineDelegate and register plugins in
didInitializeImplicitFlutterEngine, and every binaryMessenger consumer must stop
reading window?.rootViewController, which is nil during didFinishLaunching under
scenes. AppDelegate.swift:113-114 force-unwraps that nil today, which is the
SIGABRT above.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
ios/Runner/Info-Dev.plist is checked in, not generated-and-ignored, and the dev
and raybanDat xcconfigs point INFOPLIST_FILE at it. Adding the manifest only to
Info.plist therefore left a gap: `setup.sh ios` regenerates the dev plist and
picks it up, but building the dev flavour straight from Xcode uses the stale
tracked copy and still fails to launch.

Regenerated with scripts/generate_ios_dev_info_plist.sh so the tracked file
matches its source. The diff is the 19-line manifest and nothing else;
NSAllowsLocalNetworking is unchanged.

Worth a maintainer view: a generated file being tracked is the underlying smell
here. Either it stays tracked and must be regenerated whenever Info.plist
changes, or it should be gitignored and always generated.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
…the app delegate

Second half of the UIScene migration for BasedHardware#11568. Under the UIScene lifecycle the
app delegate owns no window at launch, so every binaryMessenger derived from
window?.rootViewController is nil in didFinishLaunching — and registering plugins
against the app delegate as the registry is itself fatal.

Measured on iPhone 17 Pro / iOS 27.0 with a file-backed probe: the log line before
GeneratedPluginRegistrant.register(with: self) wrote, the line after it never did,
and the process died. That is why this is not merely a nil-messenger problem.

Moves into didInitializeImplicitFlutterEngine(_:), sourcing the registry from
engineBridge.pluginRegistry and the messenger from
engineBridge.applicationRegistrar.messenger():

  - GeneratedPluginRegistrant registration
  - OmiPhoneCallsPlugin (now via registry.registrar(forPlugin:))
  - Watch/WatchRecorder, BLE, Ray-Ban Meta and phone-mic Pigeon APIs
  - notifyOnKill, apple_reminders, apple_health, speech, environment,
    audioSession, battery_widget channels and WifiNetworkPlugin

No window?.rootViewController reference remains in the file, and all nine
force unwraps of it are gone — each was an independent crash.

Left alone deliberately: SwiftFlutterForegroundTaskPlugin's registrant callback
and the top-level registerPlugins(registry:), which both receive a registry from
their caller and are already scene-independent.

Incidental fix: a launch carrying a deep link previously hit an early
`return true` in didFinishLaunching and skipped ALL channel setup. Registration no
longer shares a code path with link handling.

Still outstanding for BasedHardware#11568, not in this commit: applicationWillEnterForeground
never fires under scenes (measured, 0 of 4 foregrounds, while the notification
fires 4 of 4), so OmiBleManager.reconnectStalePeripherals() silently stops
running. That is a behavioural change and deserves its own commit.

Verification: `flutter build ios --flavor dev --debug` compiles with zero Swift
diagnostics. It cannot be signed on this branch — upstream/main hardcodes team
9536L8KLMP and unsuffixed bundle ids, which is what BasedHardware#7641 fixes — so runtime
verification is done with those signing changes applied on top.

Failure-Class: none

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011qmdS9crg5qFhan7PCbWvZ
…hook

applicationWillEnterForeground never fires under the UIScene lifecycle —
measured 0/4 on iPhone 17 Pro / iOS 27.0 across two full background ->
reopen -> swipe-kill cycles, while the app-level
UIApplication.willEnterForegroundNotification fired 4/4 in the same runs.
Apple's scene-based apps get sceneWillEnterForeground instead; the
delegate override silently stops running with no crash and no log.

AppDelegate.swift previously called OmiBleManager.shared
.reconnectStalePeripherals() from that dead override, so BLE peripherals
would never reconnect when the app returns to the foreground. Moved the
call to a NotificationCenter observer registered in
didInitializeImplicitFlutterEngine, which runs unconditionally once per
launch and isn't gated behind the deep-link early return in
didFinishLaunching.

applicationWillTerminate is left alone — measured to fire 2/2, so
notifyOnKill and disconnectAllPeripherals() already work correctly.

Part of the BasedHardware#11568 UIScene migration (step 5 of the plan). Every other
app-delegate lifecycle override in this file was already accounted for:
applicationWillEnterForeground and applicationWillTerminate are the only
two, and both are now measured.

Failure-Class: none

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Info.plist and Info-Dev.plist both set UIMainStoryboardFile = Main, but
there is no Main.storyboard in ios/Runner/ — only devLaunchScreen.storyboard
and prodLaunchScreen.storyboard (the built bundle contains only those two
.storyboardc). The app-delegate lifecycle tolerated the dangling reference;
UISceneStoryboardFile could not be set to the same nonexistent name without
aborting at launch (see the manifest added in 05b3312), which is what
surfaced this as worth fixing rather than leaving as a latent oddity.

Removed from both plists with PlistBuddy; plutil -lint passes on each.
Answers open question 3 on BasedHardware#11570.

Failure-Class: none

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ise was wrong

05b3312 omitted UISceneStoryboardFile from the scene manifest because
"this project has no Main.storyboard." That was never actually true —
Base.lproj/Main.storyboard exists, is a stock FlutterViewController scene,
and is correctly wired into Runner.xcodeproj's PBXResourcesBuildPhase for
both prod and dev targets. The false claim traces to `ls ios/Runner/*.storyboard`,
which doesn't recurse into the .lproj subdirectory Xcode puts localized
resources in.

Omitting the key meant UIKit never auto-instantiated a root view controller
for the scene on connect, so the window stayed blank — no FlutterViewController
ever got created, and consequently didInitializeImplicitFlutterEngine (where
all of 4e766a7's migrated plugin/channel registration lives) never fired.
This was invisible to devicectl-only launch testing today, because a debug-mode
Flutter build refuses to create an engine outside Xcode/Flutter tooling ("Cannot
create a FlutterEngine instance in debug mode without Flutter tooling or Xcode")
and silently no-ops instead of crashing — so "process alive, no crash" proved
nothing about whether the app was actually rendering anything.

Verified by adding the key back and rebuilding in --profile mode (launchable
via plain devicectl, sidestepping the debug-mode engine restriction): a
temporary probe confirmed didInitializeImplicitFlutterEngine now fires. The
app then hit an unrelated crash — an invalid placeholder GOOGLE_APP_ID in the
dev flavor's local GoogleService-Info.plist (a pre-existing, untracked,
community-build-local-harness placeholder file, not caused by this change).

Failure-Class: none

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cursor
cursor Bot force-pushed the fix/ios-uiscene-lifecycle branch from c6b94d3 to d6b6cf7 Compare September 2, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ios needs-maintainer-review Needs a human maintainer to sign off before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants