Skip to content

feat: migrate to SceneDelegate#53744

Open
artus9033 wants to merge 59 commits into
react:mainfrom
artus9033:feat/scene-delegate
Open

feat: migrate to SceneDelegate#53744
artus9033 wants to merge 59 commits into
react:mainfrom
artus9033:feat/scene-delegate

Conversation

@artus9033

@artus9033 artus9033 commented Sep 11, 2025

Copy link
Copy Markdown
Contributor

Summary:

This PR introduces a migration from the now-deprecated iOS AppDelegate to SceneDelegate, as well as the changes to React Native code that will migrate to APIs allowing it to work fine with the SceneDelegate approach. There are two primary goals of this API:

  • feature: ability for users to leverage resizable windows feature on iPadOS in RN apps
  • stay ahead of the incoming changes, since AppDelegate is scheduled to be deleted in future releases of iOS

The changes are additive to RN and maintain backwards compatibility, but require user-space modifications to RN apps that want to migrate from AppDelegate to the scene lifecycle.

What changed

Bootstrap (Libraries/AppDelegate)

  • RCTSceneDelegate - introduced fast integration point for subclassing in the user-space SceneDelegate base class (counterpart to RCTAppDelegate)
  • RCTReactNativeFactory - SceneDelegate entrypoints via UISceneConnectionOptions
  • RCTAppDelegate retained; deprecation wording updated to point scene apps to RCTSceneDelegate

Utilities (React/Base)

  • RCTGetActiveReactNativeFactory() - resolve factory from SceneDelegate or AppDelegate
  • RCTKeyWindow() - iOS 13–14 fallback for scene-based apps
  • RCTIsSceneDelegateApp() - dual-path linking detection

Linking (Libraries/LinkingIOS)

  • SceneDelegate class methods on RCTLinkingManager; AppDelegate methods retained for non-scene apps

Core modules

  • RCTDeviceInfo - refresh cached getConstants() on orientation/frame/content-size changes
  • NativeDeviceInfo JS - remove stale constants caching
  • RCTDevLoadingView - layout/dismiss fixes for scene window resizing

Sample apps

  • RNTester (default): RCTSceneDelegate; AppDelegate handles push notifications
  • RNTester (AppDelegate) scheme: legacy path via RNTESTER_USE_APPDELEGATE
  • HelloWorld: SceneDelegate default; legacy bootstrap in AppDelegate.legacy.swift

The RFC documenting the details is available here.

Example usage:

An example entrypoint using the added RCTSceneDelegate (imported from React_RCTAppDelegate) is:

internal import Expo
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import UIKit

class SceneDelegate: RCTSceneDelegate {
  override func scene(
    _ scene: UIScene,
    willConnectTo session: UISceneSession,
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    moduleName = "main"
    dependencyProvider = RCTAppDependencyProvider()
    automaticallyLoadReactNativeWindow = false

    guard let windowScene = scene as? UIWindowScene else {
      return
    }

    let delegate = ReactNativeDelegate()
    delegate.dependencyProvider = RCTAppDependencyProvider()
    reactNativeFactory = ExpoReactNativeFactory(delegate: delegate)

    window = UIWindow(windowScene: windowScene)

    reactNativeFactory?.startReactNative(
      withModuleName: moduleName ?? "main",
      in: window,
      initialProperties: initialProps,
      connectionOptions: connectionOptions
    )
  }
}

class ReactNativeDelegate: ExpoReactNativeFactoryDelegate {
  override func bundleURL() -> URL? {
#if DEBUG
    return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry")
#else
    return Bundle.main.url(forResource: "main", withExtension: "jsbundle")
#endif
  }
}

Follow-ups:

After merging this PR, the AppDelegate will be deprecated, but still available. After a future de-integration of AppDelegate, packages/react-native/Libraries/AppDelegate and contained files should be renamed.

Additional information

One capability that can be leveraged with the SceneDelegate on iPadOS is multi-window (multi-instance) support, which would cause all sorts of problems in the current React Native core and 3p libraries' designs. Therefore, enabling this capability is currently unsupported by React Native and to enforce this, a warning message like the one below would be printed provided this capability had been enabled:

image

Changelog:

[IOS] [ADDED] - SceneDelegate support with backwards compatibility to AppDelegate-only approaches
[IOS] [ADDED] - RCTSceneDelegate, RCTGetActiveReactNativeFactory(), and RCTReactNativeFactory SceneDelegate entrypoints
[IOS] [ADDED] - RCTLinkingManager SceneDelegate lifecycle methods (AppDelegate path retained)
[IOS] [CHANGED] - DeviceInfo to update the value returned by getConstants upon frame change
[IOS] [CHANGED] - RCTDevLoadingView layout for scene window resizing
[IOS] [ADDED] - additional scheme: RNTester (AppDelegate), allowing to run RNTester with AppDelegate infrastructure
[IOS] [ADDED] - print warning from RCTReactNativeFactory if UIApplicationSceneManifest.UIApplicationSupportsMultipleScenes in Info.plist is set to true, which is unsupported by RN to prevent bugs due to simultaneously running multiple instances of an app

Test Plan:

  • Evaluate that RNTester works properly (default RNTester SceneDelegate and RNTester (AppDelegate) schemes)
  • Verify HelloWorld SceneDelegate bootstrap; deep links and iPadOS window resize / Dimensions updates
IMG_0015
RNTester
ScreenRecording_07-13-2026.12-26-27_1.mov
HelloWorld
ScreenRecording_07-13-2026.13-15-08_1.mov

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Sep 11, 2025
@artus9033 artus9033 changed the title Feat/scene delegate feat: migrate to SceneDelegate Sep 11, 2025
@artus9033 artus9033 force-pushed the feat/scene-delegate branch 3 times, most recently from 40c091e to 5b4208c Compare November 13, 2025 11:54
@cipolleschi cipolleschi marked this pull request as ready for review February 4, 2026 11:10
@cipolleschi cipolleschi marked this pull request as draft February 4, 2026 11:10
Introduces an optional RCTSceneDelegate base class alongside the existing
RCTAppDelegate path, with RCTGetActiveReactNativeFactory and RCTKeyWindow
improvements.
Adopt RCTSceneDelegate in sample apps, wire push notifications on AppDelegate
for the scene path, fix native example views to use RCTGetActiveReactNativeFactory,
and preserve legacy AppDelegate bootstrap behind compile flags.
@artus9033 artus9033 force-pushed the feat/scene-delegate branch from e2482e9 to a0b8944 Compare July 9, 2026 15:29
@artus9033 artus9033 marked this pull request as ready for review July 13, 2026 10:12
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Jul 13, 2026
@artus9033 artus9033 force-pushed the feat/scene-delegate branch from abf677c to d5a5a6d Compare July 13, 2026 11:19

@cipolleschi cipolleschi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hi @artus9033, thanks for working on this.
The PR is very detailed and well prepared.

I think we can simplify some assumption to make it smaller and more maintainable.
I left some comments in the PR. Could you address them?

Comment on lines +279 to +282
if (supportsMultipleScenes) {
RCTLogWarn(
@"RCTReactNativeFactory: (WARNING - UNSUPPORTED RN APP CONFIGURATION) Your application is running with the Info.plist UIApplicationSceneManifest.UIApplicationSupportsMultipleScenes key set to true, which is NOT supported by React Native at the moment. Allowing the user to run multiple windows of a RN application means allowing to run multiple instances of React Native and libraries in the same process, which may cause ALL SORTS OF PROBLEMS in implementations which use singletons or static storage lifetime variables.");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if it is not supported, can't we crash? I think we should raise an exception. A warning in console is very easy to miss. Perhaps we can add a compiler flag that transforms the crash in a warning, but at least users will have to do it themselves and hopefully they'll know what they are doing.
WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree, we can make this an assertion. I think the option to opt out with a compiler flag is sensible. I do not foresee use cases when users would enable this, because even if they would do so, internal React Native components that use NSNotificationCenter would cross-talk across scenes.

* - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const std::string &)name
* jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker;
*/
@interface RCTSceneDelegate : RCTDefaultReactNativeFactoryDelegate <UIWindowSceneDelegate>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not a fan of the subclassing. Can't we move away from the subclassing approach and show the users how to use the RCTReactNativeFactory directly? What are they going to miss if they don't use this subclass?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's purely a utility to reduce the amount of code the users would need to write. I don't strongly suggest this approach, we can instead document what they need to add and drop this helper.

Comment on lines +82 to +87
#if !defined(RCT_REMOVE_LEGACY_ARCH)
@property (nonatomic, nullable) RCTBridge *bridge
__attribute__((deprecated("The bridge is deprecated and will be removed when removing the legacy architecture.")));
@property (nonatomic, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter __attribute__((
deprecated("The bridge adapter is deprecated and will be removed when removing the legacy architecture.")));
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This can be removed. It is not possible to disable New Arch

Comment on lines +58 to +66
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts
{
[RCTLinkingManager scene:scene openURLContexts:URLContexts];
}

- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity
{
[RCTLinkingManager scene:scene continueUserActivity:userActivity];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We probably need to update the website with these. I would not add the RCTSceneDelegate at all.

Comment on lines +127 to +132
if (!RCTIsSceneDelegateApp()) {
[RCTLinkingManager postNotificationWithURL:URL];
return YES;
}
return YES;

return NO;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: I typically prefer not to have negation logic. I find it easier to read the code.

Suggested change
if (!RCTIsSceneDelegateApp()) {
[RCTLinkingManager postNotificationWithURL:URL];
return YES;
}
return YES;
return NO;
if (RCTIsSceneDelegateApp()) {
return NO;
}
[RCTLinkingManager postNotificationWithURL:URL];
return YES;

#endif

#import "RCTAppDelegate.h"
#import "RCTSceneDelegate.h"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this can go

}

export default AccessibilityManagerTest;
module.exports = AccessibilityManagerTest;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we move these js changes to a different PR? They are unrelated from the SceneDelegate.

* LICENSE file in the root directory of this source tree.
*/

#if RNTESTER_USE_APPDELEGATE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not sure it is worth to complicate RNTester like this.
Also, I don't want to show a pattern to the user that is not the best one.

I would probably simplify the PR and drop the AppDelegate completely.

#import <RCTSceneDelegate.h>
#import <UIKit/UIKit.h>

@interface SceneDelegate : RCTSceneDelegate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what would it be if we do not extend the RCTSceneDelegate?

@@ -0,0 +1,65 @@
/*

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same as above, let's simplify and get rid of this.

@artus9033 artus9033 force-pushed the feat/scene-delegate branch from 27e1c80 to 49816a3 Compare July 13, 2026 15:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants