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); }} />