From 3e079f0802931635dc569f281fb48204c30e6525 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Wed, 29 Mar 2023 15:30:26 -0700 Subject: [PATCH] Make UIManager.dispatchViewManagerCommand work in Fabric through Interop Layer (#36578) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/36578 This change brings to the Fabric Interop Layer the possibility to directly call native methods on the View Manager, something that was not possible before and that we can use to simplify the migration to the New Architecture. [iOS][Added] - Native Components can now call native methods in Fabric when they are loaded through the Interop Layer Reviewed By: sammy-SC Differential Revision: D43945278 fbshipit-source-id: fe67ac85a5d0db3747105f56700d1dbba7ada5f1 --- .../Libraries/ReactNative/UIManager.js | 27 ++++++++++ ...acyViewManagerInteropCoordinatorAdapter.mm | 2 +- .../RCTLegacyViewManagerInteropCoordinator.h | 5 +- .../RCTLegacyViewManagerInteropCoordinator.mm | 51 ++++++++++++++++++- .../ios/RNTMyLegacyNativeViewManager.mm | 25 +++++++++ .../js/MyLegacyViewNativeComponent.js | 20 +++++++- .../NativeComponentExample/js/MyNativeView.js | 12 +++-- 7 files changed, 132 insertions(+), 10 deletions(-) diff --git a/packages/react-native/Libraries/ReactNative/UIManager.js b/packages/react-native/Libraries/ReactNative/UIManager.js index 71c124f3120..d9d54407296 100644 --- a/packages/react-native/Libraries/ReactNative/UIManager.js +++ b/packages/react-native/Libraries/ReactNative/UIManager.js @@ -175,6 +175,33 @@ const UIManager = { ); } }, + + dispatchViewManagerCommand( + reactTag: number, + commandName: number | string, + commandArgs: any[], + ) { + if (isFabricReactTag(reactTag)) { + const FabricUIManager = nullthrows(getFabricUIManager()); + const shadowNode = + FabricUIManager.findShadowNodeByTag_DEPRECATED(reactTag); + if (shadowNode) { + // Transform the accidental CommandID into a CommandName which is the stringified number. + // The interop layer knows how to convert this number into the right method name. + // Stringify a string is a no-op, so it's safe. + commandName = `${commandName}`; + FabricUIManager.dispatchCommand(shadowNode, commandName, commandArgs); + } + } else { + UIManagerImpl.dispatchViewManagerCommand( + reactTag, + // We have some legacy components that are actually already using strings. ¯\_(ツ)_/¯ + // $FlowFixMe[incompatible-call] + commandName, + commandArgs, + ); + } + }, }; module.exports = UIManager; diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm index 1a9611ea5eb..951db3af44f 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm @@ -50,7 +50,7 @@ - (void)handleCommand:(NSString *)commandName args:(NSArray *)args { - [_coordinator handleCommand:commandName args:args reactTag:_tag]; + [_coordinator handleCommand:commandName args:args reactTag:_tag paperView:self.paperView]; } @end diff --git a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h index 242574f05ce..0e226c6e90c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h +++ b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h @@ -33,7 +33,10 @@ typedef void (^InterceptorBlock)(std::string eventName, folly::dynamic event); - (NSString *)componentViewName; -- (void)handleCommand:(NSString *)commandName args:(NSArray *)args reactTag:(NSInteger)tag; +- (void)handleCommand:(NSString *)commandName + args:(NSArray *)args + reactTag:(NSInteger)tag + paperView:(UIView *)paperView; @end diff --git a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.mm b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.mm index 44ef7bf5183..5b22004daf1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.mm +++ b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.mm @@ -100,13 +100,23 @@ using namespace facebook::react; return RCTDropReactPrefixes(_componentData.name); } -- (void)handleCommand:(NSString *)commandName args:(NSArray *)args reactTag:(NSInteger)tag +- (void)handleCommand:(NSString *)commandName + args:(NSArray *)args + reactTag:(NSInteger)tag + paperView:(nonnull UIView *)paperView { Class managerClass = _componentData.managerClass; [self _lookupModuleMethodsIfNecessary]; RCTModuleData *moduleData = [_bridge.batchedBridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)]; id method; - if ([commandName isKindOfClass:[NSNumber class]]) { + + // We can't use `[NSString intValue]` as "0" is a valid command, + // but also a falsy value. [NSNumberFormatter numberFromString] returns a + // `NSNumber *` which is NULL when it's to be NULL + // and it points to 0 when the string is @"0" (not a falsy value). + NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; + + if ([commandName isKindOfClass:[NSNumber class]] || [formatter numberFromString:commandName] != NULL) { method = moduleData ? moduleData.methods[[commandName intValue]] : _moduleMethods[[commandName intValue]]; } else if ([commandName isKindOfClass:[NSString class]]) { method = moduleData ? moduleData.methodsByName[commandName] : _moduleMethodsByName[commandName]; @@ -121,12 +131,14 @@ using namespace facebook::react; NSArray *newArgs = [@[ [NSNumber numberWithInteger:tag] ] arrayByAddingObjectsFromArray:args]; if (_bridge) { + [self _addViewToRegistry:paperView withTag:tag]; [_bridge.batchedBridge dispatchBlock:^{ [method invokeWithBridge:self->_bridge module:self->_componentData.manager arguments:newArgs]; [self->_bridge.uiManager setNeedsLayout]; } queue:RCTGetUIManagerQueue()]; + [self _removeViewFromRegistryWithTag:tag]; } else { // TODO T86826778 - Figure out which queue this should be dispatched to. [method invokeWithBridge:nil module:self->_componentData.manager arguments:newArgs]; @@ -134,6 +146,41 @@ using namespace facebook::react; } #pragma mark - Private +- (void)_addViewToRegistry:(UIView *)view withTag:(NSInteger)tag +{ + [self _addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + if ([viewRegistry objectForKey:@(tag)] != NULL) { + return; + } + NSMutableDictionary *mutableViewRegistry = + (NSMutableDictionary *)viewRegistry; + [mutableViewRegistry setObject:view forKey:@(tag)]; + }]; +} + +- (void)_removeViewFromRegistryWithTag:(NSInteger)tag +{ + [self _addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + if ([viewRegistry objectForKey:@(tag)] == NULL) { + return; + } + + NSMutableDictionary *mutableViewRegistry = + (NSMutableDictionary *)viewRegistry; + [mutableViewRegistry removeObjectForKey:@(tag)]; + }]; +} + +- (void)_addUIBlock:(RCTViewManagerUIBlock)block +{ + __weak __typeof__(self) weakSelf = self; + [_bridge.batchedBridge + dispatchBlock:^{ + __typeof__(self) strongSelf = weakSelf; + [strongSelf->_bridge.uiManager addUIBlock:block]; + } + queue:RCTGetUIManagerQueue()]; +} // This is copy-pasta from RCTModuleData. - (void)_lookupModuleMethodsIfNecessary diff --git a/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm b/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm index f591d26f7d8..fb5cdf809f2 100644 --- a/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm +++ b/packages/rn-tester/NativeComponentExample/ios/RNTMyLegacyNativeViewManager.mm @@ -30,9 +30,34 @@ RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) RCT_EXPORT_VIEW_PROPERTY(onColorChanged, RCTBubblingEventBlock) +RCT_EXPORT_METHOD(changeBackgroundColor : (nonnull NSNumber *)reactTag color : (NSString *)color) +{ + [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + UIView *view = viewRegistry[reactTag]; + if (!view || ![view isKindOfClass:[RNTLegacyView class]]) { + RCTLogError(@"Cannot find RNTLegacyView with tag #%@", reactTag); + return; + } + + unsigned rgbValue = 0; + NSString *colorString = [NSString stringWithCString:std::string([color UTF8String]).c_str() + encoding:[NSString defaultCStringEncoding]]; + NSScanner *scanner = [NSScanner scannerWithString:colorString]; + [scanner setScanLocation:1]; // bypass '#' character + [scanner scanHexInt:&rgbValue]; + + UIColor *newColor = [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 + green:((rgbValue & 0xFF00) >> 8) / 255.0 + blue:(rgbValue & 0xFF) / 255.0 + alpha:1.0]; + view.backgroundColor = newColor; + }]; +} + - (UIView *)view { RNTLegacyView *view = [[RNTLegacyView alloc] init]; + view.backgroundColor = UIColor.redColor; return view; } diff --git a/packages/rn-tester/NativeComponentExample/js/MyLegacyViewNativeComponent.js b/packages/rn-tester/NativeComponentExample/js/MyLegacyViewNativeComponent.js index 3ed7e1f295f..c3ce7f6a6e5 100644 --- a/packages/rn-tester/NativeComponentExample/js/MyLegacyViewNativeComponent.js +++ b/packages/rn-tester/NativeComponentExample/js/MyLegacyViewNativeComponent.js @@ -8,9 +8,11 @@ * @format */ +import * as React from 'react'; import type {HostComponent} from 'react-native'; import type {ViewProps} from 'react-native/Libraries/Components/View/ViewPropTypes'; -import {requireNativeComponent} from 'react-native'; +import {requireNativeComponent, UIManager} from 'react-native'; +import ReactNative from '../../../react-native/Libraries/Renderer/shims/ReactNative'; type ColorChangedEvent = { nativeEvent: { @@ -32,6 +34,22 @@ type NativeProps = $ReadOnly<{| export type MyLegacyViewType = HostComponent; +export function callNativeMethodToChangeBackgroundColor( + viewRef: React.ElementRef | null, + color: string, +) { + if (!viewRef) { + console.log('viewRef is null'); + return; + } + UIManager.dispatchViewManagerCommand( + ReactNative.findNodeHandle(viewRef), + UIManager.getViewManagerConfig('RNTMyLegacyNativeView').Commands + .changeBackgroundColor, + [color], + ); +} + export default (requireNativeComponent( 'RNTMyLegacyNativeView', ): HostComponent); diff --git a/packages/rn-tester/NativeComponentExample/js/MyNativeView.js b/packages/rn-tester/NativeComponentExample/js/MyNativeView.js index 24eefb84fc5..e83eb3007dd 100644 --- a/packages/rn-tester/NativeComponentExample/js/MyNativeView.js +++ b/packages/rn-tester/NativeComponentExample/js/MyNativeView.js @@ -10,13 +10,14 @@ import * as React from 'react'; import {useRef, useState} from 'react'; -import {View, Button, Text} from 'react-native'; +import {View, Button, Text, UIManager} from 'react-native'; import RNTMyNativeView, { Commands as RNTMyNativeViewCommands, } from './MyNativeViewNativeComponent'; import RNTMyLegacyNativeView from './MyLegacyViewNativeComponent'; +import type {MyLegacyViewType} from './MyLegacyViewNativeComponent'; import type {MyNativeViewType} from './MyNativeViewNativeComponent'; -import {UIManager} from 'react-native'; +import {callNativeMethodToChangeBackgroundColor} from './MyLegacyViewNativeComponent'; const colors = [ '#0000FF', @@ -53,8 +54,8 @@ class HSBA { // This is an example component that migrates to use the new architecture. export default function MyNativeView(props: {}): React.Node { const ref = useRef | null>(null); + const legacyRef = useRef | null>(null); const [opacity, setOpacity] = useState(1.0); - const [color, setColor] = useState('#FF0000'); const [hsba, setHsba] = useState(new HSBA()); return ( @@ -62,9 +63,9 @@ export default function MyNativeView(props: {}): React.Node { Legacy View setHsba( new HSBA( @@ -85,12 +86,13 @@ export default function MyNativeView(props: {}): React.Node { title="Change Background" onPress={() => { let newColor = colors[Math.floor(Math.random() * 5)]; - setColor(newColor); RNTMyNativeViewCommands.callNativeMethodToChangeBackgroundColor( // $FlowFixMe[incompatible-call] ref.current, newColor, ); + + callNativeMethodToChangeBackgroundColor(legacyRef.current, newColor); }} />