diff --git a/docs/_integration-with-existing-apps-ios.md b/docs/_integration-with-existing-apps-ios.md index a0255f672a4..023de18bc85 100644 --- a/docs/_integration-with-existing-apps-ios.md +++ b/docs/_integration-with-existing-apps-ios.md @@ -248,9 +248,16 @@ React Native initialization is now unbound to any specific part of an iOS app. React Native can be initialized using a class called `RCTReactNativeFactory`, that takes care of handling the React Native lifecycle for you. -Once the class is initialized, you can either start a React Native view providing a `UIWindow` object, or you can ask for the factory to generate a `UIView` that you can load in any `UIViewController.` +:::note[`RCTAppDelegate` is deprecated] +`RCTAppDelegate` is deprecated and will be removed in a future version of React Native. New apps should bootstrap from an app-owned `SceneDelegate` (see [Bootstrapping with SceneDelegate](#6-bootstrapping-with-scenedelegate)). Existing apps that subclass `RCTAppDelegate` should migrate to `RCTReactNativeFactory` directly. +::: + +Once the class is initialized, you can either: -In the following example, we will create a ViewController that can load a React Native view as it's `view`. +- bootstrap a full app from your `SceneDelegate` (see [Bootstrapping with SceneDelegate](#6-bootstrapping-with-scenedelegate)), or +- generate a `UIView` from the factory and load it in any `UIViewController` (the approach below). + +In the following example, we will create a ViewController that can load a React Native view as its `view`. #### Create the ReactViewController @@ -268,7 +275,7 @@ Now open the `ReactViewController.m` file and apply the following changes +#import +#import +#import -+#import ++#import @interface ReactViewController () @@ -461,7 +468,222 @@ Finally, make sure to add the `UIViewControllerBasedStatusBarAppearance` key int ![Disable UIViewControllerBasedStatusBarAppearance](/docs/assets/disable-UIViewControllerBasedStatusBarAppearance.png) -## 6. Test your integration +## 6. Bootstrapping with SceneDelegate + +If your app uses the UIScene lifecycle (the default for apps created with recent versions of Xcode), React Native bootstrap happens in your app-owned `SceneDelegate`. Your `AppDelegate` stays as the process entry point (`@main`) and handles `UIApplication`-level callbacks such as push notifications. + +To bootstrap React Native with SceneDelegate: + +1. Declare `UIApplicationSceneManifest` in `Info.plist` and point it at your `SceneDelegate` class. +2. Keep `UIApplicationSupportsMultipleScenes` set to `false`. React Native does not support multi-window / multi-instance apps yet; enabling this key causes React Native to fail during initialization due to an intentional assertion, unless you explicitly define `RN_ALLOW_MULTIPLE_SCENES` on your app target. +3. Subclass `RCTDefaultReactNativeFactoryDelegate`, conform to `UIWindowSceneDelegate`, create an `RCTReactNativeFactory`, and call `startReactNativeWithModuleName:inWindow:connectionOptions:` from `scene:willConnectToSession:options:`. +4. Forward deep links from your `SceneDelegate` to `RCTLinkingManager`. +5. Keep push notifications and other `UIApplicationDelegate` callbacks on your `AppDelegate`. + +### Info.plist + +Add a scene manifest similar to the Community Template. At minimum you need: + +```xml +UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + +``` + +For Objective-C apps, use your SceneDelegate class name directly instead of `$(PRODUCT_MODULE_NAME).SceneDelegate`. + +### Create the SceneDelegate + + + + +Create `SceneDelegate.h`: + +```objc title="SceneDelegate.h" +#import +#import +#import + +@interface SceneDelegate : RCTDefaultReactNativeFactoryDelegate + +@property (nonatomic, strong, nullable) UIWindow *window; +@property (nonatomic, strong, nullable) RCTReactNativeFactory *reactNativeFactory; + +@end +``` + +Create `SceneDelegate.m`: + +```objc title="SceneDelegate.m" +#import "SceneDelegate.h" + +#import +#import +#import + +@implementation SceneDelegate + +- (void)scene:(UIScene *)scene + willConnectToSession:(UISceneSession *)session + options:(UISceneConnectionOptions *)connectionOptions +{ + if (![scene isKindOfClass:[UIWindowScene class]]) { + return; + } + + self.dependencyProvider = [RCTAppDependencyProvider new]; + self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self]; + + UIWindowScene *windowScene = (UIWindowScene *)scene; + self.window = [[UIWindow alloc] initWithWindowScene:windowScene]; + + [self.reactNativeFactory startReactNativeWithModuleName:@"HelloWorld" + inWindow:self.window + connectionOptions:connectionOptions]; +} + +- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts +{ + [RCTLinkingManager scene:scene openURLContexts:URLContexts]; +} + +- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity +{ + [RCTLinkingManager scene:scene continueUserActivity:userActivity]; +} + +- (NSURL *)bundleURL +{ +#if DEBUG + return [RCTBundleURLProvider.sharedSettings jsBundleURLForBundleRoot:@"index"]; +#else + return [NSBundle.mainBundle URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +@end +``` + + + + +Create `SceneDelegate.swift`: + +```swift title="SceneDelegate.swift" +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider +import UIKit + +class SceneDelegate: RCTDefaultReactNativeFactoryDelegate, UIWindowSceneDelegate { + var window: UIWindow? + var reactNativeFactory: RCTReactNativeFactory? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + dependencyProvider = RCTAppDependencyProvider() + reactNativeFactory = RCTReactNativeFactory(delegate: self) + window = UIWindow(windowScene: windowScene) + + reactNativeFactory?.startReactNative( + withModuleName: "HelloWorld", + in: window, + connectionOptions: connectionOptions + ) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + RCTLinkingManager.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + RCTLinkingManager.scene(scene, continue: userActivity) + } + + override func bundleURL() -> URL? { + #if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") + #else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif + } +} +``` + + + + +### Application delegate responsibilities + +Your `AppDelegate` can remain minimal and only handle callbacks that belong to the application object, such as push notifications: + + + + +```objc title="AppDelegate.m" +#import "AppDelegate.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + return YES; +} + +@end +``` + + + + +```swift title="AppDelegate.swift" +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + true + } +} +``` + + + + +:::note +React Native currently supports a single React instance per process. If `UIApplicationSupportsMultipleScenes` is `true`, React Native fails during initialization by default (when using the New Architecture). Define `RN_ALLOW_MULTIPLE_SCENES=1` in your app target's `GCC_PREPROCESSOR_DEFINITIONS` (or pass `-DRN_ALLOW_MULTIPLE_SCENES=1` via `OTHER_CFLAGS`) only if you understand the risks (e.g. cross-talk of internal RN modules, or third-party libraries) and want to downgrade the failure to a warning. +::: + +:::tip[`reactNativeFactory` field] +It is important that you expose `reactNativeFactory` on your `SceneDelegate` if you need utilities such as `RCTGetActiveReactNativeFactory()` to resolve the active factory at runtime. +::: + +## 7. Test your integration You have completed all the basic steps to integrate React Native with your application. Now we will start the [Metro bundler](https://metrobundler.dev/) to build your TypeScript application code into a bundle. Metro's HTTP server shares the bundle from `localhost` on your developer environment to a simulator or device. This allows for [hot reloading](https://reactnative.dev/blog/2016/03/24/introducing-hot-reloading). @@ -528,7 +750,7 @@ REACT_NATIVE_XCODE="$REACT_NATIVE_PATH/scripts/react-native-xcode.sh" Now, if you build your app for Release, it will work as expected. -## 7. Passing initial props to the React Native view +## 8. Passing initial props to the React Native view In some case, you'd like to pass some information from the Native app to JavaScript. For example, you might want to pass the user id of the currently logged user to React Native, together with a token that can be used to retrieve information from a database. @@ -606,6 +828,45 @@ These changes will tell React Native that your App component is now accepting so ### Update the Native code to pass the initial properties to JavaScript. +#### SceneDelegate bootstrap + + + + +Modify `SceneDelegate.m` to pass initial properties when starting React Native: + +```diff title="SceneDelegate.m" + [self.reactNativeFactory startReactNativeWithModuleName:@"HelloWorld" + inWindow:self.window ++ initialProperties:@{ ++ @"userID": @"12345678", ++ @"token": @"secretToken" ++ } + connectionOptions:connectionOptions]; +``` + + + + +Modify `SceneDelegate.swift` to pass initial properties when starting React Native: + +```diff title="SceneDelegate.swift" + reactNativeFactory?.startReactNative( + withModuleName: "HelloWorld", + in: window, ++ initialProperties: [ ++ "userID": "12345678", ++ "token": "secretToken" ++ ], + connectionOptions: connectionOptions + ) +``` + + + + +#### Embedded view controller + diff --git a/docs/fabric-native-components-ios.md b/docs/fabric-native-components-ios.md index baad9bdb6d2..ecb4b75df37 100644 --- a/docs/fabric-native-components-ios.md +++ b/docs/fabric-native-components-ios.md @@ -61,6 +61,7 @@ Podfile ... Demo ├── AppDelegate.swift +├── SceneDelegate.swift ... // highlight-start ├── RCTWebView.h diff --git a/docs/images.md b/docs/images.md index 0c547ebb34c..c4f532f53e1 100644 --- a/docs/images.md +++ b/docs/images.md @@ -264,7 +264,7 @@ Image decoding can take more than a frame-worth of time. This is one of the majo ## Configuring iOS Image Cache Limits -On iOS, we expose an API to override React Native's default image cache limits. This should be called from within your native AppDelegate code (e.g. within `didFinishLaunchingWithOptions`). +On iOS, we expose an API to override React Native's default image cache limits. Call this early during app startup from your `SceneDelegate` (in `scene:willConnectToSession:options:`) or from `AppDelegate` if you are not using the UIScene lifecycle (for example, within `application:didFinishLaunchingWithOptions:`). ```objectivec RCTSetImageCacheLimits(4*1024*1024, 200*1024*1024); diff --git a/docs/linking.md b/docs/linking.md index 308d1660d61..ca9e84cb50f 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -54,7 +54,53 @@ If you wish to receive the intent in an existing instance of MainActivity, you m :::note -On iOS, you'll need to add the `LinkingIOS` folder into your header search paths as described in step 3 [here](linking-libraries-ios#step-3). If you also want to listen to incoming app links during your app's execution, you'll need to add the following lines to your `*AppDelegate.m`: +On iOS, you'll need to add the `LinkingIOS` folder into your header search paths as described in step 3 [here](linking-libraries-ios#step-3). If you also want to listen to incoming app links during your app's execution, forward deep links from your `SceneDelegate`. If your app declares `UIApplicationSceneManifest` in `Info.plist`, `RCTLinkingManager` ignores the `AppDelegate` linking methods below - you must forward links from `SceneDelegate` instead. + + + + +```objc title="SceneDelegate.m" +#import + +- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts +{ + [RCTLinkingManager scene:scene openURLContexts:URLContexts]; +} +``` + +If your app is using [Universal Links](https://developer.apple.com/ios/universal-links/), add the following as well: + +```objc title="SceneDelegate.m" +- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity +{ + [RCTLinkingManager scene:scene continueUserActivity:userActivity]; +} +``` + + + + +```swift title="SceneDelegate.swift" +func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + RCTLinkingManager.scene(scene, openURLContexts: URLContexts) +} +``` + +If your app is using [Universal Links](https://developer.apple.com/ios/universal-links/), add the following as well: + +```swift title="SceneDelegate.swift" +func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + RCTLinkingManager.scene(scene, continue: userActivity) +} +``` + + + + +
+Apps without UIScene lifecycle + +If your app does not use `SceneDelegate`, add the following lines to your `AppDelegate` instead: @@ -110,6 +156,8 @@ func application( +
+ :::
diff --git a/docs/network.md b/docs/network.md index 1266f8cb0d3..414629e3d66 100644 --- a/docs/network.md +++ b/docs/network.md @@ -268,7 +268,26 @@ For some applications it may be appropriate to provide a custom `NSURLSessionCon #import ``` -`RCTSetCustomNSURLSessionConfigurationProvider` should be called early in the application life cycle such that it is readily available when needed by React, for instance: +`RCTSetCustomNSURLSessionConfigurationProvider` should be called early in the application life cycle such that it is readily available when needed by React. + +For apps using the UIScene lifecycle, call it from `scene:willConnectToSession:options:` before starting React Native: + +```objectivec +- (void)scene:(UIScene *)scene + willConnectToSession:(UISceneSession *)session + options:(UISceneConnectionOptions *)connectionOptions +{ + RCTSetCustomNSURLSessionConfigurationProvider(^NSURLSessionConfiguration *{ + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + // configure the session + return configuration; + }); + + // set up React Native +} +``` + +For apps without the UIScene lifecycle, call it from `application:didFinishLaunchingWithOptions:` before starting React Native: ```objectivec -(void)application:(__unused UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { diff --git a/docs/pushnotificationios.md b/docs/pushnotificationios.md index c42d3a28193..293fac1017d 100644 --- a/docs/pushnotificationios.md +++ b/docs/pushnotificationios.md @@ -22,6 +22,8 @@ To enable push notifications, [configure your notifications with Apple](https:// Then, [enable remote notifications](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app#2980038) in your project. This will automatically enable the required settings. +Push notification registration and delivery callbacks belong to `UIApplicationDelegate`, so implement them in your `AppDelegate` even when React Native is bootstrapped from `SceneDelegate`. + ### Enable support for `register` events In your `AppDelegate.m`, add: diff --git a/docs/releases/release-levels.md b/docs/releases/release-levels.md index b42f21c45a5..b2dcbcb732e 100644 --- a/docs/releases/release-levels.md +++ b/docs/releases/release-levels.md @@ -44,7 +44,27 @@ The build system generates different feature flag override classes for each rele ### iOS -The `RCTReactNativeFactory` class now has an initializer that accepts a `releaseLevel` parameter. The feature flag setup uses this parameter to select the correct feature flag overrides. +The `RCTReactNativeFactory` class now has an initializer that accepts a `releaseLevel` parameter. The feature flag setup uses this parameter to select the correct feature flag overrides. Create the factory from your `SceneDelegate` (or `AppDelegate` if you are not using the UIScene lifecycle). + + + + +```objc title="SceneDelegate.m" +self.reactNativeFactory = [[RCTReactNativeFactory alloc] initWithDelegate:self releaseLevel:Canary]; +``` + + + + +```swift title="SceneDelegate.swift" +reactNativeFactory = RCTReactNativeFactory(delegate: self, releaseLevel: RCTReleaseLevel.Canary) +``` + + + + +
+Apps without UIScene lifecycle @@ -63,4 +83,6 @@ let factory = RCTReactNativeFactory(delegate: delegate, releaseLevel: RCTRelease +
+ The system ensures that only one release level is active per app instance, and will crash if multiple factories are created with different release levels.