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
This commit is contained in:
Riccardo Cipolleschi
2023-03-29 15:30:26 -07:00
committed by Riccardo Cipolleschi
parent 18137348ee
commit 3e079f0802
7 changed files with 132 additions and 10 deletions
@@ -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;
@@ -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
@@ -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
@@ -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<RCTBridgeMethod> 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<NSNumber *, UIView *> *viewRegistry) {
if ([viewRegistry objectForKey:@(tag)] != NULL) {
return;
}
NSMutableDictionary<NSNumber *, UIView *> *mutableViewRegistry =
(NSMutableDictionary<NSNumber *, UIView *> *)viewRegistry;
[mutableViewRegistry setObject:view forKey:@(tag)];
}];
}
- (void)_removeViewFromRegistryWithTag:(NSInteger)tag
{
[self _addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
if ([viewRegistry objectForKey:@(tag)] == NULL) {
return;
}
NSMutableDictionary<NSNumber *, UIView *> *mutableViewRegistry =
(NSMutableDictionary<NSNumber *, UIView *> *)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
@@ -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<NSNumber *, UIView *> *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;
}
@@ -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<NativeProps>;
export function callNativeMethodToChangeBackgroundColor(
viewRef: React.ElementRef<MyLegacyViewType> | 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<NativeProps>);
@@ -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<React.ElementRef<MyNativeViewType> | null>(null);
const legacyRef = useRef<React.ElementRef<MyLegacyViewType> | null>(null);
const [opacity, setOpacity] = useState(1.0);
const [color, setColor] = useState('#FF0000');
const [hsba, setHsba] = useState<HSBA>(new HSBA());
return (
<View style={{flex: 1}}>
@@ -62,9 +63,9 @@ export default function MyNativeView(props: {}): React.Node {
<RNTMyNativeView ref={ref} style={{flex: 1}} opacity={opacity} />
<Text style={{color: 'red'}}>Legacy View</Text>
<RNTMyLegacyNativeView
ref={legacyRef}
style={{flex: 1}}
opacity={opacity}
color={color}
onColorChanged={event =>
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);
}}
/>
<Button