Skip to content

Adopt the iOS UIScene lifecycle - #6179

Merged
j0ntz merged 2 commits into
developfrom
jon/ios-scene-lifecycle-migration
Sep 4, 2026
Merged

Adopt the iOS UIScene lifecycle#6179
j0ntz merged 2 commits into
developfrom
jon/ios-scene-lifecycle-migration

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Description

Asana: https://app.asana.com/0/1215088146871429/1214464087258597

Adopts the iOS UIScene lifecycle, which is the first of the Track 2 items on
that task and the one that does not depend on the React Native 0.86 upgrade.

iOS 26 deprecates the UIApplicationDelegate window and lifecycle callbacks once
an app ships a scene manifest, and Apple removes the UIDesignRequiresCompatibility
opt-out in Xcode 27 (around September 2026), which is the deadline this is sized
against. The task audit flagged three deprecated call sites in AppDelegate.swift;
all three are the scene work: UIWindow(frame: UIScreen.main.bounds),
applicationDidEnterBackground, and applicationWillEnterForeground.

What changed:

  • New ios/edge/SceneDelegate.swift owns the window, the foreground/background
    privacy overlay, deep links, and home-screen quick actions.
  • A reconnecting scene (iOS can disconnect one to reclaim resources without
    killing the process) reattaches the running root view controller into its new
    window instead of starting React Native a second time, and forwards whatever
    arrived with the reconnect (quick action, URL contexts, user activities)
    through the running-app handlers, since only the first mount reads launch
    options.
  • Info.plist declares a single-scene UIApplicationSceneManifest;
    AppDelegate vends the matching configuration and keeps app-wide duties
    (Sentry, Firebase, background fetch, the React Native factory).
  • applicationIconBadgeNumber, deprecated since iOS 17, becomes
    UNUserNotificationCenter.setBadgeCount, guarded for the iOS 15.6 deployment
    target.
  • src/docs/ios-26-scene-lifecycle-migration.md records what is still owed before
    Xcode 27, with a per-surface list for the compatibility-flag removal.

Two React Native 0.79 details are worth a reviewer's attention:

  1. RCTLinkingManager has no scene entry points, and getInitialURL reads the
    cold-launch URL from bridge.launchOptions. iOS stops putting deep-link keys
    into didFinishLaunchingWithOptions once a scene manifest exists, so the scene
    rebuilds those launch options from UIScene.ConnectionOptions. Without it,
    Linking.getInitialURL() returns null on every cold-launch deep link and
    nothing crashes to signal it.
  2. AppDelegate keeps a window property. @react-native-firebase/messaging
    reads application.delegate.window on
    UIApplicationDidFinishLaunchingNotification; a delegate with no window at
    all raises doesNotRecognizeSelector and the app aborts on launch. The scene
    mirrors its window into it. react-native-contacts, react-native-share and
    RN's own LogBox read the same property, so the mirror covers them too.

Out of scope here, and tracked in the doc: removing UIDesignRequiresCompatibility
(needs full-app visual QA on iOS 26) and the React Navigation upgrade (sequence it
after React Native 0.86).

Testing

Driven on an iOS 18.6 simulator against a debug build. Temporary logging and
input-forcing harnesses were used where noted, and all of it was removed before
commit; git status is clean of them.

Driven with no forcing:

  • Cold launch reaches the wallet list and a Buy $500 quote resolves, so window
    creation and the React Native start path work under the scene.
  • Backgrounding logs sceneDidEnterBackground; the next foreground observes the
    LaunchScreen privacy overlay present and removes it.
  • A cold launch from edge://... arrives with UIApplicationLaunchOptionsURLKey
    populated and the app raises its own "Unknown deep link format" banner, proving
    the URL reached JS through getInitialURL.
  • A warm edge://... reaches scene(_:openURLContexts:).
  • setBadgeCount renders the requested badge on the home-screen icon.
  • The Release configuration compiles (xcodebuild -configuration Release).

Driven with a forced input, because the OS cannot deliver the real trigger here:

  • Home-screen quick actions, warm and cold. SpringBoard's icon context menu does
    not open under the test harness, so the real registered item (contact_support,
    read back out of UIApplication.shortcutItems) was replayed through
    windowScene(_:performActionFor:) and through the cold
    connectionOptions.shortcutItem path. Both end in Safari on the action's URL.

  • Universal links, warm and cold. edge.app serves a 404 at
    /.well-known/apple-app-site-association and Apple's CDN has no entry for the
    domain, so iOS never hands the app a browsing-web activity. A synthesized
    NSUserActivity was pushed through the real launchOptions(from:) and through
    scene(_:continue:); the app answered "Parsing link..." and "No wallets exist
    that support this link." respectively.

  • Scene disconnect and reconnect. iOS will not disconnect a single-scene iPhone
    app on demand, so the old window was released and the real
    scene(_:willConnectTo:) was replayed on a fresh SceneDelegate instance, the
    way the OS does it. The same UIViewController came back in the new window and
    the app stayed live on the Buy scene with its quote still refreshing: no second
    mount, no BootSplash re-run, no re-login.

That association gap is pre-existing and unrelated to this change, but it does mean
universal links are dead app-wide until an AASA is published.


Note

Medium Risk
Touches the iOS launch path, deep linking, and background privacy overlay with hand-rolled RN/Firebase compatibility shims that must stay correct across cold launch, warm links, and scene reconnect.

Overview
Moves the iOS app onto the UIScene lifecycle so window creation and scene callbacks are no longer on the deprecated AppDelegate path required before Xcode 27.

A new SceneDelegate owns the window, the LaunchScreen privacy overlay on background/foreground, deep links and universal links (via RCTLinkingManager), and home-screen quick actions. AppDelegate keeps global setup (Sentry, Firebase, RN factory, background fetch) and returns the scene configuration from Info.plist’s single-scene manifest. On scene reconnect, the existing React root is reattached instead of starting RN again, with late-arriving URLs/quick actions routed through the warm handlers.

Two RN 0.79 shims ship with this: rebuild launchOptions from UIScene.ConnectionOptions so cold-start Linking.getInitialURL() still works, and mirror the scene window onto AppDelegate.window so Firebase Messaging and other libraries that read delegate.window do not crash at launch. Background-fetch badge updates use UNUserNotificationCenter.setBadgeCount on iOS 16+ instead of the deprecated icon badge API.

src/docs/ios-26-scene-lifecycle-migration.md documents follow-up work (e.g. removing UIDesignRequiresCompatibility) before the Xcode 27 deadline.

Reviewed by Cursor Bugbot for commit bd90701. Bugbot is set up for automated code reviews on this repo. Configure here.

@j0ntz

j0ntz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence

buy quote under scene lifecycle

buy quote under scene lifecycle

cold launch deep link reached js

cold launch deep link reached js

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz marked this pull request as ready for review August 28, 2026 02:45
@j0ntz
j0ntz force-pushed the jon/ios-scene-lifecycle-migration branch from 9edd5d1 to 4119db1 Compare August 28, 2026 02:45
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@j0ntz
j0ntz force-pushed the jon/ios-scene-lifecycle-migration branch from 4119db1 to 43ac2b5 Compare August 28, 2026 22:40
@j0ntz

j0ntz commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

📸🪓 Test evidence (verification gaps closed)

🪓 Hack-forced evidence: quick actions: replayed the real registered contact_support item through the scene handlers because SpringBoard's icon menu is unreachable from the harness; universal links: synthesized an NSUserActivity because edge.app publishes no AASA so iOS cannot deliver one. Temporary uncommitted edit, reverted before commit; the marked frames prove the rendering, not the trigger.

badge setbadgecount

badge setbadgecount

🪓 HACK-FORCED: universal link cold launch

🪓 HACK-FORCED: universal link cold launch

🪓 HACK-FORCED: quick action cold launch

🪓 HACK-FORCED: quick action cold launch

🪓 HACK-FORCED: quick action warm before

🪓 HACK-FORCED: quick action warm before

🪓 HACK-FORCED: quick action warm after

🪓 HACK-FORCED: quick action warm after

🪓 HACK-FORCED: universal link warm continue

🪓 HACK-FORCED: universal link warm continue

Captured by the agent's in-app test run (build-and-test).

@j0ntz
j0ntz force-pushed the jon/ios-scene-lifecycle-migration branch from 43ac2b5 to c05e922 Compare August 28, 2026 22:52
Comment thread ios/edge/SceneDelegate.swift
@j0ntz
j0ntz force-pushed the jon/ios-scene-lifecycle-migration branch from c05e922 to f661dbc Compare September 1, 2026 00:32
@cursor

cursor Bot commented Sep 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@j0ntz

j0ntz commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

📸 Test evidence (scene reconnect guard)

app running before reconnect

app running before reconnect

buy scene before reconnect

buy scene before reconnect

after scene reconnect

after scene reconnect

Captured by the agent's in-app test run (build-and-test).

iOS 26 deprecates the UIApplicationDelegate window and lifecycle
callbacks once an app ships a scene manifest, and Xcode 27 is expected
to remove the UIDesignRequiresCompatibility opt-out we currently rely
on. Move window creation, foreground/background transitions, deep
links and home-screen quick actions onto a new SceneDelegate, and
leave AppDelegate owning only app-wide concerns.

React Native 0.79 has no scene-aware entry points, so the scene
rebuilds the launch options RCTLinkingManager.getInitialURL reads from
the scene connection options. Without that, a cold-launch deep link
would resolve to null.

Also replaces applicationIconBadgeNumber, deprecated since iOS 17.
Records what the UIScene adoption covers, the two React Native 0.79
workarounds it needed, how it was verified on the simulator, and the
surfaces that still have to be normalized before Xcode 27 removes the
UIDesignRequiresCompatibility opt-out.
@j0ntz
j0ntz force-pushed the jon/ios-scene-lifecycle-migration branch from f661dbc to bd90701 Compare September 4, 2026 07:10
@j0ntz
j0ntz enabled auto-merge September 4, 2026 07:10
@j0ntz
j0ntz merged commit a015c85 into develop Sep 4, 2026
7 checks passed
@j0ntz
j0ntz deleted the jon/ios-scene-lifecycle-migration branch September 4, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants