diff --git a/packages/react-native-host/cocoa/RNXHostConfig.h b/packages/react-native-host/cocoa/RNXHostConfig.h index c14aa71d99..5ab9e8e101 100644 --- a/packages/react-native-host/cocoa/RNXHostConfig.h +++ b/packages/react-native-host/cocoa/RNXHostConfig.h @@ -3,6 +3,16 @@ #import #import +#ifdef __cplusplus +#include +#endif + +#if __has_include() +#import +#endif + +@class ReactNativeHost; + NS_ASSUME_NONNULL_BEGIN /// Configuration object for ``ReactNativeHost``. @@ -19,6 +29,14 @@ NS_ASSUME_NONNULL_BEGIN /// Returns whether the bridge should be released when the app is backgrounded. @property (nonatomic, readonly) BOOL shouldReleaseBridgeWhenBackgrounded; +#if __has_include() +/// Auxiliary ``RCTTurboModuleManagerDelegate`` consulted before +/// ``RNXTurboModuleAdapter``'s defaults. For each delegate method the +/// adapter implements, the auxiliary is called via ``respondsToSelector:`` +/// first; returning ``nil`` / ``Nil`` falls through to default behavior. +@property (nonatomic, readonly, weak, nullable) id turboModuleManagerDelegate; +#endif + /// Logs a message. - (void)logWithLevel:(RCTLogLevel)level source:(RCTLogSource)source @@ -30,6 +48,20 @@ NS_ASSUME_NONNULL_BEGIN /// Handles a fatal error. - (void)onFatalError:(NSError *)error; +/// Called after the JS instance has finished loading. ``error`` is ``nil`` +/// on success. +- (void)host:(ReactNativeHost *)host didLoadInstanceWithError:(nullable NSError *)error + __attribute__((__swift_name__("host(_:didLoadInstanceWithError:)"))); + +/// Called when the instance is about to be unloaded. +- (void)hostWillUnloadInstance:(ReactNativeHost *)host; + +#ifdef __cplusplus +/// Called after host bindings install but before the user JS bundle loads. +/// Use to evaluate pre-user JS on the runtime. Bridgeless mode only. +- (void)host:(ReactNativeHost *)host didInitializeRuntime:(facebook::jsi::Runtime &)runtime; +#endif // __cplusplus + // MARK: - RCTBridgeDelegate deprecated details (for backwards compatibility) [>=0.84] - (NSURL *__nullable)sourceURLForBridge:(RCTBridge *)bridge; diff --git a/packages/react-native-host/cocoa/RNXTurboModuleAdapter.h b/packages/react-native-host/cocoa/RNXTurboModuleAdapter.h index 995d7dab9a..bb4482125d 100644 --- a/packages/react-native-host/cocoa/RNXTurboModuleAdapter.h +++ b/packages/react-native-host/cocoa/RNXTurboModuleAdapter.h @@ -8,6 +8,7 @@ #endif // USE_FABRIC @class RCTBridge; +@protocol RNXHostConfig; NS_ASSUME_NONNULL_BEGIN @@ -17,6 +18,10 @@ NS_ASSUME_NONNULL_BEGIN @interface RNXTurboModuleAdapter : NSObject #endif // USE_FABRIC +/// Weak reference to the host's config; powers consultation of +/// ``RNXHostConfig.turboModuleManagerDelegate``. +@property (nonatomic, weak, nullable) id hostConfig; + - (std::unique_ptr)jsExecutorFactoryForBridge: (RCTBridge *)bridge; diff --git a/packages/react-native-host/cocoa/RNXTurboModuleAdapter.mm b/packages/react-native-host/cocoa/RNXTurboModuleAdapter.mm index ce183860b5..2034082e37 100644 --- a/packages/react-native-host/cocoa/RNXTurboModuleAdapter.mm +++ b/packages/react-native-host/cocoa/RNXTurboModuleAdapter.mm @@ -1,5 +1,7 @@ #import "RNXTurboModuleAdapter.h" +#import "RNXHostConfig.h" + #include "FollyConfig.h" #if USE_FABRIC @@ -103,6 +105,16 @@ - (instancetype)init - (Class)getModuleClassFromName:(char const *)name { + id config = _hostConfig; + if ([config respondsToSelector:@selector(turboModuleManagerDelegate)]) { + id aux = [config turboModuleManagerDelegate]; + if ([aux respondsToSelector:_cmd]) { + Class cls = [aux getModuleClassFromName:name]; + if (cls != Nil) { + return cls; + } + } + } return RCTCoreModulesClassProvider(name); } @@ -120,6 +132,16 @@ - (Class)getModuleClassFromName:(char const *)name - (id)getModuleInstanceFromClass:(Class)moduleClass { + id config = _hostConfig; + if ([config respondsToSelector:@selector(turboModuleManagerDelegate)]) { + id aux = [config turboModuleManagerDelegate]; + if ([aux respondsToSelector:_cmd]) { + id instance = [aux getModuleInstanceFromClass:moduleClass]; + if (instance != nil) { + return instance; + } + } + } #if USE_OSS_CODEGEN return RCTAppSetupDefaultModuleFromClass(moduleClass, [RCTAppDependencyProvider new]); #elif __has_include() || __has_include() diff --git a/packages/react-native-host/cocoa/ReactNativeHost.mm b/packages/react-native-host/cocoa/ReactNativeHost.mm index 677b9e8a38..d13c525950 100644 --- a/packages/react-native-host/cocoa/ReactNativeHost.mm +++ b/packages/react-native-host/cocoa/ReactNativeHost.mm @@ -35,6 +35,41 @@ @interface ReactNativeHost () @end #endif // USE_CODEGEN_PROVIDER +#if USE_BRIDGELESS + +// Forwards host:didInitializeRuntime: from RCTHost to the consumer's RNXHostConfig. +@interface _RNXForwardingRCTHostDelegate : NSObject +- (instancetype)initWithHost:(ReactNativeHost *)host config:(id)config; +@end + +@implementation _RNXForwardingRCTHostDelegate { + __weak ReactNativeHost *_host; + __weak id _config; +} + +- (instancetype)initWithHost:(ReactNativeHost *)host config:(id)config +{ + if (self = [super init]) { + _host = host; + _config = config; + } + return self; +} + +- (void)host:(RCTHost *)host didInitializeRuntime:(facebook::jsi::Runtime &)runtime +{ + id config = _config; + ReactNativeHost *forwardedHost = _host; + if (forwardedHost != nil && + [config respondsToSelector:@selector(host:didInitializeRuntime:)]) { + [config host:forwardedHost didInitializeRuntime:runtime]; + } +} + +@end + +#endif // USE_BRIDGELESS + @implementation ReactNativeHost { __weak id _config; NSDictionary *_launchOptions; @@ -44,6 +79,9 @@ @implementation ReactNativeHost { RCTHost *_reactHost; NSLock *_isShuttingDown; RNXHostReleaser *_hostReleaser; +#if USE_BRIDGELESS + _RNXForwardingRCTHostDelegate *_hostDelegateProxy; +#endif // USE_BRIDGELESS #ifdef USE_REACT_NATIVE_CONFIG std::shared_ptr _reactNativeConfig; #endif // USE_REACT_NATIVE_CONFIG @@ -99,11 +137,63 @@ - (instancetype)initWithConfig:(id)config launchOptions:(NSDictio }); } + if ([config respondsToSelector:@selector(host:didLoadInstanceWithError:)]) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(_rnxInstanceDidLoad:) + name:RCTJavaScriptDidLoadNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(_rnxInstanceDidFailToLoad:) + name:RCTJavaScriptDidFailToLoadNotification + object:nil]; + } + if ([config respondsToSelector:@selector(hostWillUnloadInstance:)]) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(_rnxInstanceWillUnload:) + name:RCTBridgeWillBeInvalidatedNotification + object:nil]; + } + [self initializeReactHost]; } return self; } +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)_rnxInstanceDidLoad:(NSNotification *)__unused notification +{ + id config = _config; + if ([config respondsToSelector:@selector(host:didLoadInstanceWithError:)]) { + [config host:self didLoadInstanceWithError:nil]; + } +} + +- (void)_rnxInstanceDidFailToLoad:(NSNotification *)notification +{ + id config = _config; + if ([config respondsToSelector:@selector(host:didLoadInstanceWithError:)]) { + NSError *error = notification.userInfo[@"error"]; + [config host:self didLoadInstanceWithError:error ?: [NSError errorWithDomain:@"ReactNativeHost" + code:0 + userInfo:nil]]; + } +} + +- (void)_rnxInstanceWillUnload:(NSNotification *)__unused notification +{ + id config = _config; + if ([config respondsToSelector:@selector(hostWillUnloadInstance:)]) { + [config hostWillUnloadInstance:self]; + } +} + - (RCTBridge *)bridge { if (self.isBridgelessEnabled) { @@ -264,6 +354,7 @@ - (void)enableTurboModule { #if USE_FABRIC _turboModuleAdapter = [[RNXTurboModuleAdapter alloc] init]; + _turboModuleAdapter.hostConfig = _config; RCTEnableTurboModule(true); #endif } @@ -303,6 +394,10 @@ - (void)initializeReactHost #endif }; + // Retained as an ivar because RCTHost stores host delegates weakly. + _hostDelegateProxy = [[_RNXForwardingRCTHostDelegate alloc] initWithHost:self + config:_config]; + __weak __typeof(self) weakSelf = self; if ([RCTHost instancesRespondToSelector:@selector (initWithBundleURLProvider: @@ -312,13 +407,13 @@ - (void)initializeReactHost initWithBundleURLProvider:^{ return [weakSelf sourceURLForBridge:nil]; } - hostDelegate:nil + hostDelegate:_hostDelegateProxy turboModuleManagerDelegate:_turboModuleAdapter jsEngineProvider:jsEngineProvider launchOptions:_launchOptions]; } else { _reactHost = [[RCTHost alloc] initWithBundleURL:[self sourceURLForBridge:nil] - hostDelegate:nil + hostDelegate:_hostDelegateProxy turboModuleManagerDelegate:_turboModuleAdapter jsEngineProvider:jsEngineProvider]; }