Skip to content

Commit f209c13

Browse files
artus9033meta-codesync[bot]
authored andcommitted
Add SceneDelegate support (#57700)
Summary: Pull Request resolved: #57700 Add SceneDelegate lifecycle support to React Native iOS while retaining the AppDelegate path for backwards compatibility. Update linking, window resolution, bootstrap APIs, RNTester, HelloWorld, and generated API snapshots for scene-based applications. Changelog: [iOS][Added] - Add SceneDelegate lifecycle support Test Plan: - `buck2 build --flagfile fbsource//fbobjc/mode/buck2/linux --config fbobjc.react_native_debug=development --flagfile fbsource//fbobjc/mode/iphonesimulator-arm64 --flagfile fbsource//fbobjc/mode/jest-coverage --config xplat.available_platforms=CXX,APPLE --config cxx.default_platform=iphonesimulator-arm64 --config user.sandcastle_build_mode=profile --config user.platform_flavor=iphonesimulator-arm64 //xplat/js/react-native-github/packages/rn-tester:RNTesterIOS` - `xcodebuild -quiet -workspace RNTesterPods.xcworkspace -scheme RNTester -configuration Debug -destination "generic/platform=iOS Simulator" CODE_SIGNING_ALLOWED=NO build` - `arc lint` - Evaluate RNTester with the `RNTester` SceneDelegate scheme. - Evaluate RNTester with the `RNTester (AppDelegate)` legacy scheme. - Verify HelloWorld bootstrap and deep linking. Reviewed By: javache Differential Revision: D113758228 Pulled By: cipolleschi
1 parent 647c9df commit f209c13

36 files changed

Lines changed: 594 additions & 264 deletions

packages/react-native/Libraries/AppDelegate/RCTAppDelegate.h

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@
1919
NS_ASSUME_NONNULL_BEGIN
2020

2121
/**
22-
* @deprecated RCTAppDelegate is deprecated and will be removed in a future version of React Native. Use
23-
`RCTReactNativeFactory` instead.
22+
* @deprecated RCTAppDelegate is deprecated and will be removed in a future version of React Native. For new apps
23+
* using the UIScene lifecycle, implement your own `SceneDelegate` with `RCTReactNativeFactory` (see integration
24+
* docs). For AppDelegate-only apps, use `RCTReactNativeFactory` directly.
25+
*
26+
* Scene-based apps must keep `UIApplicationSupportsMultipleScenes` set to `false` in Info.plist. Define
27+
* `RN_ALLOW_MULTIPLE_SCENES` on the app target to downgrade the unsupported-configuration crash to a warning.
2428
*
2529
* The RCTAppDelegate is an utility class that implements some base configurations for all the React Native apps.
2630
* It is not mandatory to use it, but it could simplify your AppDelegate code.
@@ -55,19 +59,12 @@ NS_ASSUME_NONNULL_BEGIN
5559
* - (id<RCTTurboModule>)getModuleInstanceFromClass:(Class)moduleClass
5660
*/
5761
__attribute__((deprecated(
58-
"RCTAppDelegate is deprecated and will be removed in a future version of React Native. Use `RCTReactNativeFactory` instead.")))
62+
"RCTAppDelegate is deprecated and will be removed in a future version of React Native. For UIScene apps implement your own SceneDelegate with RCTReactNativeFactory; otherwise use RCTReactNativeFactory.")))
5963
@interface RCTAppDelegate : RCTDefaultReactNativeFactoryDelegate<UIApplicationDelegate>
6064

6165
/// The window object, used to render the UViewControllers
6266
@property (nonatomic, strong, nonnull) UIWindow *window;
6367

64-
#if !defined(RCT_REMOVE_LEGACY_ARCH)
65-
@property (nonatomic, nullable) RCTBridge *bridge
66-
__attribute__((deprecated("The bridge is deprecated and will be removed when removing the legacy architecture.")));
67-
@property (nonatomic, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter __attribute__((
68-
deprecated("The bridge adapter is deprecated and will be removed when removing the legacy architecture.")));
69-
#endif
70-
7168
@property (nonatomic, strong, nullable) NSString *moduleName;
7269
@property (nonatomic, strong, nullable) NSDictionary *initialProps;
7370
@property (nonatomic, strong) RCTReactNativeFactory *reactNativeFactory;

packages/react-native/Libraries/AppDelegate/RCTDefaultReactNativeFactoryDelegate.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ - (RCTColorSpace)defaultColorSpace
6666

6767
- (NSURL *_Nullable)bundleURL
6868
{
69-
[NSException raise:@"RCTAppDelegate::bundleURL not implemented"
69+
[NSException raise:@"RCTReactNativeFactoryDelegate::bundleURL not implemented"
7070
format:@"Subclasses must implement a valid getBundleURL method"];
7171
return nullptr;
7272
}

packages/react-native/Libraries/AppDelegate/RCTReactNativeFactory.h

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ typedef NS_ENUM(NSInteger, RCTReleaseLevel) { Canary, Experimental, Stable };
5858

5959
@interface RCTReactNativeFactory : NSObject
6060

61+
/**
62+
* Bootstrap entrypoints:
63+
* - **AppDelegate path**: `startReactNativeWithModuleName:inWindow:launchOptions:` — call from
64+
* `application:didFinishLaunchingWithOptions:` or `RCTAppDelegate`.
65+
* - **SceneDelegate path**: `startReactNativeWithModuleName:inWindow:connectionOptions:` — call from
66+
* `scene:willConnectToSession:options:` in your app-owned `SceneDelegate` (subclass
67+
* `RCTDefaultReactNativeFactoryDelegate` and conform to `UIWindowSceneDelegate`).
68+
*/
69+
6170
- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate;
6271

6372
- (instancetype)initWithDelegate:(id<RCTReactNativeFactoryDelegate>)delegate releaseLevel:(RCTReleaseLevel)releaseLevel;
@@ -73,9 +82,33 @@ typedef NS_ENUM(NSInteger, RCTReleaseLevel) { Canary, Experimental, Stable };
7382
initialProperties:(NSDictionary *_Nullable)initialProperties
7483
launchOptions:(NSDictionary *_Nullable)launchOptions;
7584

76-
#if !defined(RCT_REMOVE_LEGACY_ARCH)
77-
@property (nonatomic, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter __attribute__((
78-
deprecated("The bridgeAdapter is deprecated and will be removed when removing the legacy architecture.")));
85+
/**
86+
* SceneDelegate entrypoint to start a React Native instance with the specified module name, window, and connection
87+
* options for linking and user activity information. Only the first item in `URLContexts` and `userActivities` is used.
88+
* @param moduleName name of the JS module to load
89+
* @param window the window to launch in
90+
* @param connectionOptions the scene's connection options
91+
*/
92+
- (void)startReactNativeWithModuleName:(NSString *)moduleName
93+
inWindow:(UIWindow *_Nullable)window
94+
connectionOptions:(UISceneConnectionOptions *_Nullable)connectionOptions;
95+
96+
/**
97+
* SceneDelegate entrypoint to start a React Native instance with the specified module name, window, initial properties,
98+
* and connection options. Only the first item in `URLContexts` and `userActivities` is used.
99+
* @param moduleName name of the JS module to load
100+
* @param window the window to launch in
101+
* @param initialProperties the initial root properties
102+
* @param connectionOptions the scene's connection options
103+
*/
104+
- (void)startReactNativeWithModuleName:(NSString *)moduleName
105+
inWindow:(UIWindow *_Nullable)window
106+
initialProperties:(NSDictionary *_Nullable)initialProperties
107+
connectionOptions:(UISceneConnectionOptions *_Nullable)connectionOptions;
108+
109+
#if !RCT_REMOVE_LEGACY_ARCH
110+
@property (nonatomic, nullable) RCTBridge *bridge
111+
__attribute__((deprecated("The bridge is deprecated and will be removed when removing the legacy architecture.")));
79112
#endif
80113

81114
@property (nonatomic, strong, nonnull) RCTRootViewFactory *rootViewFactory;

packages/react-native/Libraries/AppDelegate/RCTReactNativeFactory.mm

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
#import <React/RCTBundleManager.h>
1010
#import <React/RCTColorSpaceUtils.h>
1111
#import <React/RCTDevMenu.h>
12-
#import <React/RCTLog.h>
1312
#import <React/RCTRootView.h>
1413
#import <React/RCTSurfacePresenterBridgeAdapter.h>
1514
#import <React/RCTUtils.h>
@@ -40,8 +39,36 @@ @interface RCTReactNativeFactory () <
4039
RCTHostDelegate,
4140
RCTJSRuntimeConfiguratorProtocol,
4241
RCTTurboModuleManagerDelegate>
42+
4343
@end
4444

45+
static NSDictionary *RCTConvertConnectionOptionsToLaunchOptions(UISceneConnectionOptions *connectionOptions)
46+
{
47+
NSMutableDictionary *launchOptions = [NSMutableDictionary dictionary];
48+
49+
if (connectionOptions.URLContexts.count > 0) {
50+
UIOpenURLContext *urlContext = connectionOptions.URLContexts.allObjects.firstObject;
51+
52+
if (urlContext.URL) {
53+
launchOptions[UIApplicationLaunchOptionsURLKey] = urlContext.URL;
54+
}
55+
}
56+
57+
if (connectionOptions.userActivities.count > 0) {
58+
NSUserActivity *activity = connectionOptions.userActivities.allObjects.firstObject;
59+
60+
if (activity) {
61+
NSMutableDictionary *userActivityDict = [NSMutableDictionary dictionary];
62+
userActivityDict[UIApplicationLaunchOptionsUserActivityTypeKey] = activity.activityType;
63+
userActivityDict[@"UIApplicationLaunchOptionsUserActivityKey"] = activity;
64+
65+
launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey] = userActivityDict;
66+
}
67+
}
68+
69+
return launchOptions;
70+
}
71+
4572
@implementation RCTReactNativeFactory
4673

4774
@synthesize bundleConfiguration = _bundleConfiguration;
@@ -95,6 +122,29 @@ - (void)startReactNativeWithModuleName:(NSString *)moduleName
95122
[window makeKeyAndVisible];
96123
}
97124

125+
#pragma mark - UIScene.ConnectionOptions
126+
127+
- (void)startReactNativeWithModuleName:(NSString *)moduleName
128+
inWindow:(UIWindow *_Nullable)window
129+
connectionOptions:(UISceneConnectionOptions *_Nullable)connectionOptions
130+
{
131+
[self startReactNativeWithModuleName:moduleName
132+
inWindow:window
133+
initialProperties:nil
134+
launchOptions:RCTConvertConnectionOptionsToLaunchOptions(connectionOptions)];
135+
}
136+
137+
- (void)startReactNativeWithModuleName:(NSString *)moduleName
138+
inWindow:(UIWindow *_Nullable)window
139+
initialProperties:(NSDictionary *_Nullable)initialProperties
140+
connectionOptions:(UISceneConnectionOptions *_Nullable)connectionOptions
141+
{
142+
[self startReactNativeWithModuleName:moduleName
143+
inWindow:window
144+
initialProperties:initialProperties
145+
launchOptions:RCTConvertConnectionOptionsToLaunchOptions(connectionOptions)];
146+
}
147+
98148
#pragma mark - RCTUIConfiguratorProtocol
99149

100150
- (RCTColorSpace)defaultColorSpace

packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,6 @@ typedef void (^RCTLoadSourceForBridgeBlock)(RCTBridge *bridge, RCTSourceLoadBloc
184184
*/
185185
@interface RCTRootViewFactory : NSObject
186186

187-
#if !defined(RCT_REMOVE_LEGACY_ARCH)
188-
@property (nonatomic, strong, nullable) RCTBridge *bridge;
189-
@property (nonatomic, strong, nullable) RCTSurfacePresenterBridgeAdapter *bridgeAdapter;
190-
#endif
191-
192187
@property (nonatomic, strong, nullable) RCTHost *reactHost;
193188

194189
- (instancetype)initWithConfiguration:(RCTRootViewFactoryConfiguration *)configuration

packages/react-native/Libraries/AppDelegate/RCTRootViewFactory.mm

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
#import <React/RCTRootView.h>
1212
#import <React/RCTUtils.h>
1313
#import <react/renderer/runtimescheduler/RuntimeScheduler.h>
14-
#import "RCTAppDelegate.h"
1514
#import "RCTAppSetupUtils.h"
1615

1716
#if RN_DISABLE_OSS_PLUGIN_HEADER

packages/react-native/Libraries/AppDelegate/RCTUIConfiguratorProtocol.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ NS_ASSUME_NONNULL_BEGIN
2020

2121
/**
2222
* This method can be used to customize the rootView that is passed to React Native.
23-
* A typical example is to override this method in the AppDelegate to change the background color.
24-
* To achieve this, add in your `AppDelegate.mm`:
23+
* Override on your `RCTReactNativeFactoryDelegate` (e.g. in AppDelegate, SceneDelegate, or
24+
* `RCTAppDelegate` subclass). Example:
2525
* ```
2626
* - (void)customizeRootView:(RCTRootView *)rootView
2727
* {

packages/react-native/Libraries/AppDelegate/React-RCTAppDelegate.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Pod::Spec.new do |s|
5959
s.dependency "RCTTypeSafety"
6060
s.dependency "React-RCTNetwork"
6161
s.dependency "React-RCTImage"
62+
s.dependency "React-RCTLinking"
6263
s.dependency "React-CoreModules"
6364
s.dependency "React-RCTFBReactNativeSpec"
6465
s.dependency "React-defaultsnativemodule"

packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,42 @@
1111

1212
@interface RCTLinkingManager : RCTEventEmitter
1313

14+
/**
15+
* Deep linking integration supports two iOS lifecycle paths:
16+
* - **AppDelegate methods** (below): use when the app does not declare `UIApplicationSceneManifest` in Info.plist.
17+
* - **SceneDelegate methods** (below): use when the app uses the UIScene lifecycle. Forward these from your
18+
* app-owned `SceneDelegate`.
19+
*/
20+
21+
#pragma mark - AppDelegate methods
22+
23+
/// Lifecycle method informing of a URL being opened with the app.
24+
/// Invoke from AppDelegate for non-scene apps (no `UIApplicationSceneManifest` in Info.plist).
25+
/// Note: this is an implementation using the iOS 9.0-26.0 API
1426
+ (BOOL)application:(nonnull UIApplication *)app
1527
openURL:(nonnull NSURL *)URL
1628
options:(nonnull NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options;
1729

30+
/// Lifecycle method handling a URL being opened with the app.
31+
/// Invoke from AppDelegate for non-scene apps.
32+
/// Note: this is an implementation using the iOS 4.2-9.0 API
1833
+ (BOOL)application:(nonnull UIApplication *)application
1934
openURL:(nonnull NSURL *)URL
2035
sourceApplication:(nullable NSString *)sourceApplication
2136
annotation:(nonnull id)annotation;
2237

38+
/// Lifecycle method handling user activity being performed.
39+
/// Invoke from AppDelegate for non-scene apps.
2340
+ (BOOL)application:(nonnull UIApplication *)application
2441
continueUserActivity:(nonnull NSUserActivity *)userActivity
2542
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))restorationHandler;
2643

44+
#pragma mark - SceneDelegate methods
45+
46+
/// Handles user activity for scene-based apps. Invoke from your SceneDelegate.
47+
+ (void)scene:(nonnull UIScene *)scene continueUserActivity:(nonnull NSUserActivity *)userActivity;
48+
49+
/// Handles URLs opened while the app is running for scene-based apps. Invoke from your SceneDelegate.
50+
+ (void)scene:(nonnull UIScene *)scene openURLContexts:(nonnull NSSet<UIOpenURLContext *> *)URLContexts;
51+
2752
@end

packages/react-native/Libraries/LinkingIOS/RCTLinkingManager.mm

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,38 @@
1616

1717
static NSString *const kOpenURLNotification = @"RCTOpenURLNotification";
1818

19-
static void postNotificationWithURL(NSURL *URL, id sender)
20-
{
21-
NSDictionary<NSString *, id> *payload = @{@"url" : URL.absoluteString};
22-
[[NSNotificationCenter defaultCenter] postNotificationName:kOpenURLNotification object:sender userInfo:payload];
23-
}
24-
2519
@interface RCTLinkingManager () <NativeLinkingManagerSpec>
20+
21+
/// Common logic for handling user activities originating from both AppDelegate- and SceneDelegate- lifecycle methods
22+
+ (void)handleUserActivity:(NSUserActivity *)userActivity window:(UIWindow *)window;
23+
24+
/// Common logic for handling user activities from AppDelegate-lifecycle methods.
25+
+ (BOOL)handleAppDelegateURL:(NSURL *)URL app:(UIApplication *)app;
26+
27+
/// Posts a URL notification that will be handled by the emitter to JS; this method is used to invoke instance methods
28+
/// of RCTLinkingManager from class methods via NSNotificationCenter.
29+
/// @param URL The URL to be emitted.
30+
+ (void)postNotificationWithURL:(NSURL *)URL;
31+
2632
@end
2733

2834
@implementation RCTLinkingManager
2935

3036
RCT_EXPORT_MODULE()
3137

38+
+ (void)postNotificationWithURL:(NSURL *)URL
39+
{
40+
NSDictionary<NSString *, id> *payload = @{@"url" : URL.absoluteString};
41+
[[NSNotificationCenter defaultCenter] postNotificationName:kOpenURLNotification object:nil userInfo:payload];
42+
}
43+
3244
- (dispatch_queue_t)methodQueue
3345
{
3446
return dispatch_get_main_queue();
3547
}
3648

49+
#pragma mark - RCTEventEmitter methods
50+
3751
- (void)startObserving
3852
{
3953
[[NSNotificationCenter defaultCenter] addObserver:self
@@ -52,33 +66,69 @@ - (void)stopObserving
5266
return @[ @"url" ];
5367
}
5468

69+
#pragma mark - JS methods
70+
5571
+ (BOOL)application:(UIApplication *)app
5672
openURL:(NSURL *)URL
5773
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options
5874
{
59-
postNotificationWithURL(URL, self);
60-
return YES;
75+
return [self handleAppDelegateURL:URL app:app];
6176
}
6277

63-
// Corresponding api deprecated in iOS 9
6478
+ (BOOL)application:(UIApplication *)application
6579
openURL:(NSURL *)URL
6680
sourceApplication:(NSString *)sourceApplication
6781
annotation:(id)annotation
6882
{
69-
postNotificationWithURL(URL, self);
70-
return YES;
83+
return [self handleAppDelegateURL:URL app:application];
7184
}
7285

7386
+ (BOOL)application:(UIApplication *)application
7487
continueUserActivity:(NSUserActivity *)userActivity
7588
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> *_Nullable))restorationHandler
89+
{
90+
if (RCTIsSceneDelegateApp()) {
91+
return NO;
92+
}
93+
94+
[RCTLinkingManager handleUserActivity:userActivity window:RCTKeyWindow()];
95+
return YES;
96+
}
97+
98+
#pragma mark - SceneDelegate methods
99+
100+
+ (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity
101+
{
102+
[RCTLinkingManager handleUserActivity:userActivity window:RCTKeyWindow()];
103+
}
104+
105+
+ (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts
106+
{
107+
if (URLContexts.count == 0) {
108+
return;
109+
}
110+
111+
NSURL *URL = URLContexts.allObjects.firstObject.URL;
112+
[RCTLinkingManager postNotificationWithURL:URL];
113+
}
114+
115+
#pragma mark - Common logic methods
116+
117+
+ (void)handleUserActivity:(NSUserActivity *)userActivity window:(UIWindow *)window
76118
{
77119
// This can be nullish when launching an App Clip.
78120
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb] && userActivity.webpageURL != nil) {
79-
NSDictionary *payload = @{@"url" : userActivity.webpageURL.absoluteString};
80-
[[NSNotificationCenter defaultCenter] postNotificationName:kOpenURLNotification object:self userInfo:payload];
121+
[RCTLinkingManager postNotificationWithURL:userActivity.webpageURL];
81122
}
123+
}
124+
125+
+ (BOOL)handleAppDelegateURL:(NSURL *)URL app:(UIApplication *)app
126+
{
127+
if (RCTIsSceneDelegateApp()) {
128+
return NO;
129+
}
130+
131+
[RCTLinkingManager postNotificationWithURL:URL];
82132
return YES;
83133
}
84134

0 commit comments

Comments
 (0)