Skip to content

Adopt the iOS UIScene lifecycle - #6179

Open
j0ntz wants to merge 2 commits into
developfrom
jon/ios-scene-lifecycle-migration
Open

Adopt the iOS UIScene lifecycle#6179
j0ntz wants to merge 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.
  • 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.

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
Changes core iOS startup, window ownership, deep-link cold launch, and background privacy behavior; regressions would be subtle (e.g. null initial URLs) without the new launch-options shim.

Overview
Moves iOS window creation and scene lifecycle off deprecated AppDelegate APIs by adding SceneDelegate, declaring a single-scene UIApplicationSceneManifest in Info.plist, and wiring AppDelegate to vend the scene configuration while keeping Sentry, Firebase, background fetch, and the React Native factory.

Window, privacy overlay, deep links, and quick actions now live on the scene: React Native starts in the scene-owned window (mirrored back to AppDelegate.window for Firebase and other libs that still read it), background/foreground uses the LaunchScreen privacy overlay via sceneDidEnterBackground / sceneWillEnterForeground, and linking/quick-action handlers moved from the app delegate to scene(_:openURLContexts:), scene(_:continue:), and windowScene(_:performActionFor:). Cold-launch deep links and universal links are rebuilt into launch options from UIScene.ConnectionOptions so Linking.getInitialURL() still works under RN 0.79.

Background-fetch badge updates switch from deprecated applicationIconBadgeNumber to UNUserNotificationCenter.setBadgeCount with an iOS 15 fallback. A CHANGELOG entry and ios-26-scene-lifecycle-migration.md document remaining Xcode 27 work (e.g. removing UIDesignRequiresCompatibility).

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

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.
@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).

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 43ac2b5 to c05e922 Compare August 28, 2026 22:52
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.

1 participant