diff --git a/docs/_integration-with-existing-apps-ios.md b/docs/_integration-with-existing-apps-ios.md index a0255f672a4..187959716e7 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,226 @@ Finally, make sure to add the `UIViewControllerBasedStatusBarAppearance` key int ![Disable UIViewControllerBasedStatusBarAppearance](/docs/assets/disable-UIViewControllerBasedStatusBarAppearance.png) -## 6. Test your integration +## 6. Bootstrapping with SceneDelegate + +:::info[Requires React Native 0.88 or later] +The `SceneDelegate` APIs described in this section require React Native 0.88 or later. Earlier releases do not provide the `connectionOptions:` overloads of `startReactNative` nor the `SceneDelegate` methods on `RCTLinkingManager`. +::: + +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. +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 instance per process, so multiple scenes are not supported. Keep `UIApplicationSupportsMultipleScenes` set to `false`. +::: + +:::tip[`reactNativeFactory` field] +Store the `RCTReactNativeFactory` in a property on your `SceneDelegate` so that it is retained for the lifetime of the scene. If it is only held by a local variable, it is deallocated as soon as `scene:willConnectToSession:options:` returns and React Native tears down with it. +::: + +## 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 +754,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 +832,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 0445967067d..4668e6c0998 100644 --- a/docs/images.md +++ b/docs/images.md @@ -279,7 +279,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 939292522d5..45e845c02b9 100644 --- a/docs/linking.md +++ b/docs/linking.md @@ -53,8 +53,93 @@ 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) +} +``` + + + + +These callbacks only cover links that arrive while the scene is connected. To handle linking in a cold-start (the app being launched by a custom URL scheme or a Universal Link), you also need to hand the scene's `UIScene.ConnectionOptions` to React Native when you bootstrap it, otherwise [`getInitialURL()`](#getinitialurl) resolves to `null`: + + + + +```objc title="SceneDelegate.m" +- (void)scene:(UIScene *)scene + willConnectToSession:(UISceneSession *)session + options:(UISceneConnectionOptions *)connectionOptions +{ + // ... + [self.reactNativeFactory startReactNativeWithModuleName:@"HelloWorld" + inWindow:self.window + connectionOptions:connectionOptions]; +} +``` + + + + +```swift title="SceneDelegate.swift" +func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions +) { + // ... + reactNativeFactory?.startReactNative( + withModuleName: "HelloWorld", + in: window, + connectionOptions: connectionOptions + ) +} +``` + + + + +See [Bootstrapping with SceneDelegate](integration-with-existing-apps#6-bootstrapping-with-scenedelegate) for the full `SceneDelegate` setup. + +
+Apps without UIScene lifecycle + +If your app does not use `SceneDelegate`, add the following lines to your `AppDelegate` instead: @@ -110,11 +195,13 @@ func application( -::: +
+--- + ### Handling Deep Links There are two ways to handle URLs that open your app. 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/release-levels.md b/docs/release-levels.md index 11123402880..1fa6b94702c 100644 --- a/docs/release-levels.md +++ b/docs/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.