From f61f87c5ef33cc8098f937aef4a9bd0634b16159 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 16 Feb 2024 09:39:44 -0800 Subject: [PATCH] Make the `addUIBlock`'s Dictionary works correctly (#43060) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/43060 When inestigating the reason why [`react-native-view-shot`]() ws not working, we realized that there are many libraries that follows this pattern: ```objc [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { if (UIView *view = viewRegistry[reactTag]) { //do something with the View } }]; ``` The problem is that, with the New Architecture, that view registry is usually empty, because the components are registered and tracked in another place. This make many libraries stop working when used with the New Architecture. This change introduces a class that behaves like a dictionary but that forward the calls to retrieve the view to the right place, in order to get the view that is needed. Noticably, this approach allow us also to remove some shenanigans we were applying to make sure that the interop layer could access the views wrapped in it, so the current solution is more general and should work in multiple situations. ## Changelog [iOS][Fixed] - Make sure that `addUIBlock` provides a Dictionary-look-alike object that returns the right views when queried. Reviewed By: sammy-SC Differential Revision: D53826203 fbshipit-source-id: 08d359676d69777b88fa9b18dc141187ac42dbce --- .../react-native/React/Base/RCTBridgeProxy.mm | 11 ++- ...RCTLegacyViewManagerInteropComponentView.h | 6 ++ ...CTLegacyViewManagerInteropComponentView.mm | 5 ++ ...acyViewManagerInteropCoordinatorAdapter.mm | 2 - .../react-native/React/Modules/RCTUIManager.h | 32 +++++++++ .../react-native/React/Modules/RCTUIManager.m | 71 ++++++++++++++++++- .../RCTLegacyViewManagerInteropCoordinator.h | 5 -- .../RCTLegacyViewManagerInteropCoordinator.mm | 25 ------- 8 files changed, 120 insertions(+), 37 deletions(-) diff --git a/packages/react-native/React/Base/RCTBridgeProxy.mm b/packages/react-native/React/Base/RCTBridgeProxy.mm index af6b7afb687ac7..6c1e914c2f86b7 100644 --- a/packages/react-native/React/Base/RCTBridgeProxy.mm +++ b/packages/react-native/React/Base/RCTBridgeProxy.mm @@ -414,8 +414,9 @@ - (UIView *)viewForReactTag:(NSNumber *)reactTag { [self logWarning:@"Please migrate to RCTViewRegistry: @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED." cmd:_cmd]; - return [_viewRegistry viewForReactTag:reactTag] ? [_viewRegistry viewForReactTag:reactTag] - : [_legacyViewRegistry objectForKey:reactTag]; + UIView *view = [_viewRegistry viewForReactTag:reactTag] ? [_viewRegistry viewForReactTag:reactTag] + : [_legacyViewRegistry objectForKey:reactTag]; + return [RCTUIManager paperViewOrCurrentView:view]; } - (void)addUIBlock:(RCTViewManagerUIBlock)block @@ -428,7 +429,11 @@ - (void)addUIBlock:(RCTViewManagerUIBlock)block RCTExecuteOnMainQueue(^{ __typeof(self) strongSelf = weakSelf; if (strongSelf) { - block((RCTUIManager *)strongSelf, strongSelf->_legacyViewRegistry); + RCTUIManager *proxiedManager = (RCTUIManager *)strongSelf; + RCTComposedViewRegistry *composedViewRegistry = + [[RCTComposedViewRegistry alloc] initWithUIManager:proxiedManager + andRegistry:strongSelf->_legacyViewRegistry]; + block(proxiedManager, composedViewRegistry); } }); } diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h index fae91e1b10d4b9..9ad322f0d54880 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.h @@ -21,6 +21,12 @@ NS_ASSUME_NONNULL_BEGIN + (void)supportLegacyViewManagerWithName:(NSString *)componentName; + (void)supportLegacyViewManagersWithPrefix:(NSString *)prefix; +/** + * This method is required for addUIBlock and to let the infra bypass the interop layer + * when providing views from the RCTUIManager. The interop layer should be transparent to the users. + */ +- (UIView *)paperView; + @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm index d3f4abfb771511..4dd1f7782c9dce 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropComponentView.mm @@ -263,6 +263,11 @@ - (void)_setPropsWithUpdateMask:(RNComponentViewUpdateMask)updateMask } } +- (UIView *)paperView +{ + return _adapter.paperView; +} + #pragma mark - Native Commands - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args 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 5470c02b935a6e..341de93e877f63 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/LegacyViewManagerInterop/RCTLegacyViewManagerInteropCoordinatorAdapter.mm @@ -26,7 +26,6 @@ - (instancetype)initWithCoordinator:(RCTLegacyViewManagerInteropCoordinator *)co - (void)dealloc { - [_coordinator removeViewFromRegistryWithTag:_tag]; [_paperView removeFromSuperview]; [_coordinator removeObserveForTag:_tag]; } @@ -42,7 +41,6 @@ - (UIView *)paperView weakSelf.eventInterceptor(eventName, event); } }]; - [_coordinator addViewToRegistry:_paperView withTag:_tag]; } return _paperView; } diff --git a/packages/react-native/React/Modules/RCTUIManager.h b/packages/react-native/React/Modules/RCTUIManager.h index 0979c965f8fdca..fbd8e2c166cdbf 100644 --- a/packages/react-native/React/Modules/RCTUIManager.h +++ b/packages/react-native/React/Modules/RCTUIManager.h @@ -155,6 +155,14 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier */ - (void)setNeedsLayout; +/** + * This method is used to extract the wrapped view + * from a view that might be the Interop Layer view wrapper. + * If the view passed as parameter is the Interop Layer wrapper, this method returns the wrapped view + * Otherwise, it returns the view itself. + */ ++ (UIView *)paperViewOrCurrentView:(UIView *)view; + /** * Dedicated object for subscribing for UIManager events. * See `RCTUIManagerObserver` protocol for more details. @@ -180,6 +188,30 @@ RCT_EXTERN NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplier @end +/** + * This is a composed ViewRegistry which implement the same behavior of a Dictionary. + * We need this because, when libraries use `addUIBlock` they receives both the UIManager and a Dictionary which maps + * reactTags to Views. The problem is that in the New Architecture that dictionary is always empty and many libraries + * broke because they want to access the dictionary. Instead, they should use the `uiManager viewForReactTag` method to + * retrieve the views they need. + * + * The `RCTComposedViewRegistry` follows the composite pattern and receives as inputs the `RCTUIManager`and the + * dictionary that was passed to the libraries. It extends `NSDictionary` to make sure that it has the same interface of + * the parameter that is passed. This class fixes the problem because we override the`objectForKeyedSubscript:` method + * which is used to access the dictionary. That method is now implemented looking in the dictionary registry first and + * then using the`viewForReactTag` method. + */ +@interface RCTComposedViewRegistry : NSMutableDictionary + +- (instancetype)initWithUIManager:(RCTUIManager *)uiManager andRegistry:(NSDictionary *)registry; + +@end + +// This protocol is needed to silence the "unknown selector" warning +@protocol RCTRendererInteropLayerAdapting +- (UIView *)paperView; +@end + RCT_EXTERN NSMutableDictionary *RCTModuleConstantsForDestructuredComponent( NSMutableDictionary *directEvents, NSMutableDictionary *bubblingEvents, diff --git a/packages/react-native/React/Modules/RCTUIManager.m b/packages/react-native/React/Modules/RCTUIManager.m index 1d85520d0a4a78..f5e5c7c7a82aca 100644 --- a/packages/react-native/React/Modules/RCTUIManager.m +++ b/packages/react-native/React/Modules/RCTUIManager.m @@ -374,7 +374,7 @@ - (UIView *)viewForReactTag:(NSNumber *)reactTag if (!view) { view = _viewRegistry[reactTag]; } - return view; + return [RCTUIManager paperViewOrCurrentView:view]; } - (RCTShadowView *)shadowViewForReactTag:(NSNumber *)reactTag @@ -1157,7 +1157,9 @@ - (void)flushUIBlocksWithCompletion:(void (^)(void))completion @try { for (RCTViewManagerUIBlock block in previousPendingUIBlocks) { - block(strongSelf, strongSelf->_viewRegistry); + RCTComposedViewRegistry *composedViewRegistry = + [[RCTComposedViewRegistry alloc] initWithUIManager:strongSelf andRegistry:strongSelf->_viewRegistry]; + block(strongSelf, composedViewRegistry); } } @catch (NSException *exception) { RCTLogError(@"Exception thrown while executing UI block: %@", exception); @@ -1639,6 +1641,19 @@ + (UIView *)JSResponder return _jsResponder; } ++ (UIView *)paperViewOrCurrentView:(UIView *)view +{ + if ([view respondsToSelector:@selector(paperView)]) { + return [view performSelector:@selector(paperView)]; + } + return view; +} + +- (void)removeViewFromRegistry:(NSNumber *)reactTag +{ + [_viewRegistry removeObjectForKey:reactTag]; +} + @end @implementation RCTBridge (RCTUIManager) @@ -1649,3 +1664,55 @@ - (RCTUIManager *)uiManager } @end + +@implementation RCTComposedViewRegistry { + __weak RCTUIManager *_uiManager; + NSDictionary *_registry; +} + +- (instancetype)initWithUIManager:(RCTUIManager *)uiManager andRegistry:(NSDictionary *)registry +{ + self = [super init]; + if (self) { + self->_uiManager = uiManager; + self->_registry = registry; + } + return self; +} + +- (id)objectForKey:(id)key +{ + if (![key isKindOfClass:[NSNumber class]]) { + return [super objectForKeyedSubscript:key]; + } + + NSNumber *index = (NSNumber *)key; + UIView *view = [_uiManager viewForReactTag:index]; + if (view) { + return [RCTUIManager paperViewOrCurrentView:view]; + } + view = _registry[index]; + if (view) { + return [RCTUIManager paperViewOrCurrentView:view]; + } + return [super objectForKeyedSubscript:key]; +} + +- (void)removeObjectForKey:(id)key +{ + if (![key isKindOfClass:[NSNumber class]]) { + return [super removeObjectForKey:key]; + } + NSNumber *tag = (NSNumber *)key; + + if (_registry[key]) { + NSMutableDictionary *mutableRegistry = (NSMutableDictionary *)_registry; + [mutableRegistry removeObjectForKey:tag]; + } else if ([_uiManager viewForReactTag:tag]) { + [_uiManager removeViewFromRegistry:tag]; + } else { + [super removeObjectForKey:key]; + } +} + +@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 1a39901d1e2f41..e0b1a969de22ad 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h +++ b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h @@ -39,11 +39,6 @@ typedef void (^InterceptorBlock)(std::string eventName, folly::dynamic event); args:(NSArray *)args reactTag:(NSInteger)tag paperView:(UIView *)paperView; - -- (void)removeViewFromRegistryWithTag:(NSInteger)tag; - -- (void)addViewToRegistry:(UIView *)view withTag:(NSInteger)tag; - @end NS_ASSUME_NONNULL_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 ea8222b2e81218..bb5720e122d16d 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.mm +++ b/packages/react-native/ReactCommon/react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.mm @@ -147,31 +147,6 @@ - (void)handleCommand:(NSString *)commandName } } -- (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)]; - }]; -} - #pragma mark - Private - (void)_handleCommandsOnBridge:(id)method withArgs:(NSArray *)newArgs {