mirror of
https://github.com/status-im/react-native.git
synced 2025-01-09 17:15:54 +00:00
3372541a2a
Summary: <!-- Thank you for sending the PR! We appreciate you spending the time to work on these changes. Help us understand your motivation by explaining why you decided to make this change. You can learn more about contributing to React Native here: http://facebook.github.io/react-native/docs/contributing.html Happy contributing! --> *Accidentally closed previous PR* Sometimes it can be useful to have an animated view be created with either scale X or scale Y in cases where scaleXY might not be as visually appealing. Test Plan Tested on both ios and android in the sample project: https://github.com/Liamandrew/ScaleAnimationSample ![scaleanimation](https://user-images.githubusercontent.com/30114733/37023697-d0aa7372-217a-11e8-8d3b-2958c63ad83a.gif) Closes https://github.com/facebook/react-native/pull/18220 Differential Revision: D7542334 Pulled By: hramos fbshipit-source-id: 208472e5d8f5a04ca3c3a99adce77b035e331ef1
1603 lines
55 KiB
Objective-C
1603 lines
55 KiB
Objective-C
/**
|
|
* Copyright (c) 2015-present, Facebook, Inc.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTUIManager.h"
|
|
|
|
#import <AVFoundation/AVFoundation.h>
|
|
|
|
#import "RCTAccessibilityManager.h"
|
|
#import "RCTAssert.h"
|
|
#import "RCTBridge+Private.h"
|
|
#import "RCTBridge.h"
|
|
#import "RCTComponent.h"
|
|
#import "RCTComponentData.h"
|
|
#import "RCTConvert.h"
|
|
#import "RCTDefines.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTLayoutAnimation.h"
|
|
#import "RCTLayoutAnimationGroup.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTModuleData.h"
|
|
#import "RCTModuleMethod.h"
|
|
#import "RCTProfile.h"
|
|
#import "RCTRootContentView.h"
|
|
#import "RCTRootShadowView.h"
|
|
#import "RCTRootViewInternal.h"
|
|
#import "RCTScrollableProtocol.h"
|
|
#import "RCTShadowView+Internal.h"
|
|
#import "RCTShadowView.h"
|
|
#import "RCTSurfaceRootShadowView.h"
|
|
#import "RCTSurfaceRootView.h"
|
|
#import "RCTUIManagerObserverCoordinator.h"
|
|
#import "RCTUIManagerUtils.h"
|
|
#import "RCTUtils.h"
|
|
#import "RCTView.h"
|
|
#import "RCTViewManager.h"
|
|
#import "UIView+React.h"
|
|
|
|
static void RCTTraverseViewNodes(id<RCTComponent> view, void (^block)(id<RCTComponent>))
|
|
{
|
|
if (view.reactTag) {
|
|
block(view);
|
|
|
|
for (id<RCTComponent> subview in view.reactSubviews) {
|
|
RCTTraverseViewNodes(subview, block);
|
|
}
|
|
}
|
|
}
|
|
|
|
NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification = @"RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification";
|
|
|
|
@implementation RCTUIManager
|
|
{
|
|
// Root views are only mutated on the shadow queue
|
|
NSMutableSet<NSNumber *> *_rootViewTags;
|
|
NSMutableArray<RCTViewManagerUIBlock> *_pendingUIBlocks;
|
|
|
|
// Animation
|
|
RCTLayoutAnimationGroup *_layoutAnimationGroup; // Main thread only
|
|
|
|
NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
|
|
NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only
|
|
|
|
NSMapTable<RCTShadowView *, NSArray<NSString *> *> *_shadowViewsWithUpdatedProps; // UIManager queue only.
|
|
NSHashTable<RCTShadowView *> *_shadowViewsWithUpdatedChildren; // UIManager queue only.
|
|
|
|
// Keyed by viewName
|
|
NSDictionary *_componentDataByName;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
+ (BOOL)requiresMainQueueSetup
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[NSNotificationCenter.defaultCenter removeObserver:self];
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
/**
|
|
* Called on the JS Thread since all modules are invalidated on the JS thread
|
|
*/
|
|
|
|
// This only accessed from the shadow queue
|
|
_pendingUIBlocks = nil;
|
|
|
|
RCTExecuteOnMainQueue(^{
|
|
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"UIManager invalidate", nil);
|
|
for (NSNumber *rootViewTag in self->_rootViewTags) {
|
|
UIView *rootView = self->_viewRegistry[rootViewTag];
|
|
if ([rootView conformsToProtocol:@protocol(RCTInvalidating)]) {
|
|
[(id<RCTInvalidating>)rootView invalidate];
|
|
}
|
|
}
|
|
|
|
self->_rootViewTags = nil;
|
|
self->_shadowViewRegistry = nil;
|
|
self->_viewRegistry = nil;
|
|
self->_bridge = nil;
|
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
|
|
});
|
|
}
|
|
|
|
- (NSMutableDictionary<NSNumber *, RCTShadowView *> *)shadowViewRegistry
|
|
{
|
|
// NOTE: this method only exists so that it can be accessed by unit tests
|
|
if (!_shadowViewRegistry) {
|
|
_shadowViewRegistry = [NSMutableDictionary new];
|
|
}
|
|
return _shadowViewRegistry;
|
|
}
|
|
|
|
- (NSMutableDictionary<NSNumber *, UIView *> *)viewRegistry
|
|
{
|
|
// NOTE: this method only exists so that it can be accessed by unit tests
|
|
if (!_viewRegistry) {
|
|
_viewRegistry = [NSMutableDictionary new];
|
|
}
|
|
return _viewRegistry;
|
|
}
|
|
|
|
- (void)setBridge:(RCTBridge *)bridge
|
|
{
|
|
RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance");
|
|
_bridge = bridge;
|
|
|
|
_shadowViewRegistry = [NSMutableDictionary new];
|
|
_viewRegistry = [NSMutableDictionary new];
|
|
|
|
_shadowViewsWithUpdatedProps = [NSMapTable weakToStrongObjectsMapTable];
|
|
_shadowViewsWithUpdatedChildren = [NSHashTable weakObjectsHashTable];
|
|
|
|
// Internal resources
|
|
_pendingUIBlocks = [NSMutableArray new];
|
|
_rootViewTags = [NSMutableSet new];
|
|
|
|
_observerCoordinator = [RCTUIManagerObserverCoordinator new];
|
|
|
|
// Get view managers from bridge
|
|
NSMutableDictionary *componentDataByName = [NSMutableDictionary new];
|
|
for (Class moduleClass in _bridge.moduleClasses) {
|
|
if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) {
|
|
RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass
|
|
bridge:_bridge];
|
|
componentDataByName[componentData.name] = componentData;
|
|
}
|
|
}
|
|
|
|
_componentDataByName = [componentDataByName copy];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(didReceiveNewContentSizeMultiplier)
|
|
name:RCTAccessibilityManagerDidUpdateMultiplierNotification
|
|
object:_bridge.accessibilityManager];
|
|
#if !TARGET_OS_TV
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(namedOrientationDidChange)
|
|
name:UIDeviceOrientationDidChangeNotification
|
|
object:nil];
|
|
#endif
|
|
[RCTLayoutAnimation initializeStatics];
|
|
}
|
|
|
|
#pragma mark - Event emitting
|
|
|
|
- (void)didReceiveNewContentSizeMultiplier
|
|
{
|
|
// Report the event across the bridge.
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateContentSizeMultiplier"
|
|
body:@([_bridge.accessibilityManager multiplier])];
|
|
#pragma clang diagnostic pop
|
|
|
|
RCTExecuteOnUIManagerQueue(^{
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
|
|
object:self];
|
|
[self setNeedsLayout];
|
|
});
|
|
}
|
|
|
|
#if !TARGET_OS_TV
|
|
// Names and coordinate system from html5 spec:
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Screen.orientation
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Screen.lockOrientation
|
|
static NSDictionary *deviceOrientationEventBody(UIDeviceOrientation orientation)
|
|
{
|
|
NSString *name;
|
|
NSNumber *degrees = @0;
|
|
BOOL isLandscape = NO;
|
|
switch(orientation) {
|
|
case UIDeviceOrientationPortrait:
|
|
name = @"portrait-primary";
|
|
break;
|
|
case UIDeviceOrientationPortraitUpsideDown:
|
|
name = @"portrait-secondary";
|
|
degrees = @180;
|
|
break;
|
|
case UIDeviceOrientationLandscapeRight:
|
|
name = @"landscape-primary";
|
|
degrees = @-90;
|
|
isLandscape = YES;
|
|
break;
|
|
case UIDeviceOrientationLandscapeLeft:
|
|
name = @"landscape-secondary";
|
|
degrees = @90;
|
|
isLandscape = YES;
|
|
break;
|
|
case UIDeviceOrientationFaceDown:
|
|
case UIDeviceOrientationFaceUp:
|
|
case UIDeviceOrientationUnknown:
|
|
// Unsupported
|
|
return nil;
|
|
}
|
|
return @{
|
|
@"name": name,
|
|
@"rotationDegrees": degrees,
|
|
@"isLandscape": @(isLandscape),
|
|
};
|
|
}
|
|
|
|
- (void)namedOrientationDidChange
|
|
{
|
|
NSDictionary *orientationEvent = deviceOrientationEventBody([UIDevice currentDevice].orientation);
|
|
if (!orientationEvent) {
|
|
return;
|
|
}
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"namedOrientationDidChange"
|
|
body:orientationEvent];
|
|
#pragma clang diagnostic pop
|
|
}
|
|
#endif
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return RCTGetUIManagerQueue();
|
|
}
|
|
|
|
- (void)registerRootViewTag:(NSNumber *)rootTag
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
|
|
RCTAssert(RCTIsReactRootView(rootTag),
|
|
@"Attempt to register rootTag (%@) which is not actually root tag.", rootTag);
|
|
|
|
RCTAssert(![_rootViewTags containsObject:rootTag],
|
|
@"Attempt to register rootTag (%@) which was already registred.", rootTag);
|
|
|
|
[_rootViewTags addObject:rootTag];
|
|
|
|
// Registering root shadow view
|
|
RCTSurfaceRootShadowView *shadowView = [RCTSurfaceRootShadowView new];
|
|
shadowView.reactTag = rootTag;
|
|
_shadowViewRegistry[rootTag] = shadowView;
|
|
|
|
// Registering root view
|
|
RCTExecuteOnMainQueue(^{
|
|
RCTSurfaceRootView *rootView = [RCTSurfaceRootView new];
|
|
rootView.reactTag = rootTag;
|
|
self->_viewRegistry[rootTag] = rootView;
|
|
});
|
|
}
|
|
|
|
- (void)registerRootView:(RCTRootContentView *)rootView
|
|
{
|
|
RCTAssertMainQueue();
|
|
|
|
NSNumber *reactTag = rootView.reactTag;
|
|
RCTAssert(RCTIsReactRootView(reactTag),
|
|
@"View %@ with tag #%@ is not a root view", rootView, reactTag);
|
|
|
|
UIView *existingView = _viewRegistry[reactTag];
|
|
RCTAssert(existingView == nil || existingView == rootView,
|
|
@"Expect all root views to have unique tag. Added %@ twice", reactTag);
|
|
|
|
CGSize availableSize = rootView.availableSize;
|
|
|
|
// Register view
|
|
_viewRegistry[reactTag] = rootView;
|
|
|
|
// Register shadow view
|
|
RCTExecuteOnUIManagerQueue(^{
|
|
if (!self->_viewRegistry) {
|
|
return;
|
|
}
|
|
|
|
RCTRootShadowView *shadowView = [RCTRootShadowView new];
|
|
shadowView.availableSize = availableSize;
|
|
shadowView.reactTag = reactTag;
|
|
shadowView.viewName = NSStringFromClass([rootView class]);
|
|
self->_shadowViewRegistry[shadowView.reactTag] = shadowView;
|
|
[self->_rootViewTags addObject:reactTag];
|
|
});
|
|
}
|
|
|
|
- (NSString *)viewNameForReactTag:(NSNumber *)reactTag
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
return _shadowViewRegistry[reactTag].viewName;
|
|
}
|
|
|
|
- (UIView *)viewForReactTag:(NSNumber *)reactTag
|
|
{
|
|
RCTAssertMainQueue();
|
|
return _viewRegistry[reactTag];
|
|
}
|
|
|
|
- (RCTShadowView *)shadowViewForReactTag:(NSNumber *)reactTag
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
return _shadowViewRegistry[reactTag];
|
|
}
|
|
|
|
- (void)_executeBlockWithShadowView:(void (^)(RCTShadowView *shadowView))block forTag:(NSNumber *)tag
|
|
{
|
|
RCTAssertMainQueue();
|
|
|
|
RCTExecuteOnUIManagerQueue(^{
|
|
RCTShadowView *shadowView = self->_shadowViewRegistry[tag];
|
|
|
|
if (shadowView == nil) {
|
|
RCTLogInfo(@"Could not locate shadow view with tag #%@, this is probably caused by a temporary inconsistency between native views and shadow views.", tag);
|
|
return;
|
|
}
|
|
|
|
block(shadowView);
|
|
});
|
|
}
|
|
|
|
- (void)setAvailableSize:(CGSize)availableSize forRootView:(UIView *)rootView
|
|
{
|
|
RCTAssertMainQueue();
|
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) {
|
|
RCTAssert([shadowView isKindOfClass:[RCTRootShadowView class]], @"Located shadow view is actually not root view.");
|
|
|
|
RCTRootShadowView *rootShadowView = (RCTRootShadowView *)shadowView;
|
|
|
|
if (CGSizeEqualToSize(availableSize, rootShadowView.availableSize)) {
|
|
return;
|
|
}
|
|
|
|
rootShadowView.availableSize = availableSize;
|
|
[self setNeedsLayout];
|
|
} forTag:rootView.reactTag];
|
|
}
|
|
|
|
- (void)setLocalData:(NSObject *)localData forView:(UIView *)view
|
|
{
|
|
RCTAssertMainQueue();
|
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) {
|
|
shadowView.localData = localData;
|
|
[self setNeedsLayout];
|
|
} forTag:view.reactTag];
|
|
}
|
|
|
|
/**
|
|
* TODO(yuwang): implement the nativeID functionality in a more efficient way
|
|
* instead of searching the whole view tree
|
|
*/
|
|
- (UIView *)viewForNativeID:(NSString *)nativeID withRootTag:(NSNumber *)rootTag
|
|
{
|
|
RCTAssertMainQueue();
|
|
UIView *view = [self viewForReactTag:rootTag];
|
|
return [self _lookupViewForNativeID:nativeID inView:view];
|
|
}
|
|
|
|
- (UIView *)_lookupViewForNativeID:(NSString *)nativeID inView:(UIView *)view
|
|
{
|
|
RCTAssertMainQueue();
|
|
if (view != nil && [nativeID isEqualToString:view.nativeID]) {
|
|
return view;
|
|
}
|
|
|
|
for (UIView *subview in view.subviews) {
|
|
UIView *targetView = [self _lookupViewForNativeID:nativeID inView:subview];
|
|
if (targetView != nil) {
|
|
return targetView;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
- (void)setSize:(CGSize)size forView:(UIView *)view
|
|
{
|
|
RCTAssertMainQueue();
|
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) {
|
|
if (CGSizeEqualToSize(size, shadowView.size)) {
|
|
return;
|
|
}
|
|
|
|
shadowView.size = size;
|
|
[self setNeedsLayout];
|
|
} forTag:view.reactTag];
|
|
}
|
|
|
|
- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(UIView *)view
|
|
{
|
|
RCTAssertMainQueue();
|
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) {
|
|
if (CGSizeEqualToSize(shadowView.intrinsicContentSize, intrinsicContentSize)) {
|
|
return;
|
|
}
|
|
|
|
shadowView.intrinsicContentSize = intrinsicContentSize;
|
|
[self setNeedsLayout];
|
|
} forTag:view.reactTag];
|
|
}
|
|
|
|
/**
|
|
* Unregisters views from registries
|
|
*/
|
|
- (void)_purgeChildren:(NSArray<id<RCTComponent>> *)children
|
|
fromRegistry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)registry
|
|
{
|
|
for (id<RCTComponent> child in children) {
|
|
RCTTraverseViewNodes(registry[child.reactTag], ^(id<RCTComponent> subview) {
|
|
RCTAssert(![subview isReactRootView], @"Root views should not be unregistered");
|
|
if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) {
|
|
[(id<RCTInvalidating>)subview invalidate];
|
|
}
|
|
[registry removeObjectForKey:subview.reactTag];
|
|
});
|
|
}
|
|
}
|
|
|
|
- (void)addUIBlock:(RCTViewManagerUIBlock)block
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
|
|
if (!block || !_viewRegistry) {
|
|
return;
|
|
}
|
|
|
|
[_pendingUIBlocks addObject:block];
|
|
}
|
|
|
|
- (void)prependUIBlock:(RCTViewManagerUIBlock)block
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
|
|
if (!block || !_viewRegistry) {
|
|
return;
|
|
}
|
|
|
|
[_pendingUIBlocks insertObject:block atIndex:0];
|
|
}
|
|
|
|
- (void)setNextLayoutAnimationGroup:(RCTLayoutAnimationGroup *)layoutAnimationGroup
|
|
{
|
|
RCTAssertMainQueue();
|
|
|
|
if (_layoutAnimationGroup && ![_layoutAnimationGroup isEqual:layoutAnimationGroup]) {
|
|
RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.",
|
|
[_layoutAnimationGroup description],
|
|
[layoutAnimationGroup description]);
|
|
}
|
|
|
|
_layoutAnimationGroup = layoutAnimationGroup;
|
|
}
|
|
|
|
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
|
|
NSHashTable<RCTShadowView *> *affectedShadowViews = [NSHashTable weakObjectsHashTable];
|
|
[rootShadowView layoutWithAffectedShadowViews:affectedShadowViews];
|
|
|
|
if (!affectedShadowViews.count) {
|
|
// no frame change results in no UI update block
|
|
return nil;
|
|
}
|
|
|
|
typedef struct {
|
|
CGRect frame;
|
|
UIUserInterfaceLayoutDirection layoutDirection;
|
|
BOOL isNew;
|
|
BOOL parentIsNew;
|
|
} RCTFrameData;
|
|
|
|
// Construct arrays then hand off to main thread
|
|
NSUInteger count = affectedShadowViews.count;
|
|
NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count];
|
|
NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count];
|
|
{
|
|
NSUInteger index = 0;
|
|
RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes;
|
|
for (RCTShadowView *shadowView in affectedShadowViews) {
|
|
reactTags[index] = shadowView.reactTag;
|
|
RCTLayoutMetrics layoutMetrics = shadowView.layoutMetrics;
|
|
frameDataArray[index++] = (RCTFrameData){
|
|
layoutMetrics.frame,
|
|
layoutMetrics.layoutDirection,
|
|
shadowView.isNewView,
|
|
shadowView.superview.isNewView,
|
|
};
|
|
}
|
|
}
|
|
|
|
for (RCTShadowView *shadowView in affectedShadowViews) {
|
|
|
|
// We have to do this after we build the parentsAreNew array.
|
|
shadowView.newView = NO;
|
|
|
|
NSNumber *reactTag = shadowView.reactTag;
|
|
|
|
if (shadowView.onLayout) {
|
|
CGRect frame = shadowView.layoutMetrics.frame;
|
|
shadowView.onLayout(@{
|
|
@"layout": @{
|
|
@"x": @(frame.origin.x),
|
|
@"y": @(frame.origin.y),
|
|
@"width": @(frame.size.width),
|
|
@"height": @(frame.size.height),
|
|
},
|
|
});
|
|
}
|
|
|
|
if (
|
|
RCTIsReactRootView(reactTag) &&
|
|
[shadowView isKindOfClass:[RCTRootShadowView class]]
|
|
) {
|
|
CGSize contentSize = shadowView.layoutMetrics.frame.size;
|
|
|
|
RCTExecuteOnMainQueue(^{
|
|
UIView *view = self->_viewRegistry[reactTag];
|
|
RCTAssert(view != nil, @"view (for ID %@) not found", reactTag);
|
|
|
|
RCTRootView *rootView = (RCTRootView *)[view superview];
|
|
if ([rootView isKindOfClass:[RCTRootView class]]) {
|
|
rootView.intrinsicContentSize = contentSize;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Perform layout (possibly animated)
|
|
return ^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
|
|
const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes;
|
|
RCTLayoutAnimationGroup *layoutAnimationGroup = uiManager->_layoutAnimationGroup;
|
|
|
|
__block NSUInteger completionsCalled = 0;
|
|
|
|
NSInteger index = 0;
|
|
for (NSNumber *reactTag in reactTags) {
|
|
RCTFrameData frameData = frameDataArray[index++];
|
|
|
|
UIView *view = viewRegistry[reactTag];
|
|
CGRect frame = frameData.frame;
|
|
|
|
UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection;
|
|
BOOL isNew = frameData.isNew;
|
|
RCTLayoutAnimation *updatingLayoutAnimation = isNew ? nil : layoutAnimationGroup.updatingLayoutAnimation;
|
|
BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew;
|
|
RCTLayoutAnimation *creatingLayoutAnimation = shouldAnimateCreation ? layoutAnimationGroup.creatingLayoutAnimation : nil;
|
|
|
|
void (^completion)(BOOL) = ^(BOOL finished) {
|
|
completionsCalled++;
|
|
if (layoutAnimationGroup.callback && completionsCalled == count) {
|
|
layoutAnimationGroup.callback(@[@(finished)]);
|
|
|
|
// It's unsafe to call this callback more than once, so we nil it out here
|
|
// to make sure that doesn't happen.
|
|
layoutAnimationGroup.callback = nil;
|
|
}
|
|
};
|
|
|
|
if (view.reactLayoutDirection != layoutDirection) {
|
|
view.reactLayoutDirection = layoutDirection;
|
|
}
|
|
|
|
if (creatingLayoutAnimation) {
|
|
|
|
// Animate view creation
|
|
[view reactSetFrame:frame];
|
|
|
|
CATransform3D finalTransform = view.layer.transform;
|
|
CGFloat finalOpacity = view.layer.opacity;
|
|
|
|
NSString *property = creatingLayoutAnimation.property;
|
|
if ([property isEqualToString:@"scaleXY"]) {
|
|
view.layer.transform = CATransform3DMakeScale(0, 0, 0);
|
|
} else if ([property isEqualToString:@"scaleX"]) {
|
|
view.layer.transform = CATransform3DMakeScale(0, 1, 0);
|
|
} else if ([property isEqualToString:@"scaleY"]) {
|
|
view.layer.transform = CATransform3DMakeScale(1, 0, 0);
|
|
} else if ([property isEqualToString:@"opacity"]) {
|
|
view.layer.opacity = 0.0;
|
|
} else {
|
|
RCTLogError(@"Unsupported layout animation createConfig property %@",
|
|
creatingLayoutAnimation.property);
|
|
}
|
|
|
|
[creatingLayoutAnimation performAnimations:^{
|
|
if (
|
|
[property isEqualToString:@"scaleX"] ||
|
|
[property isEqualToString:@"scaleY"] ||
|
|
[property isEqualToString:@"scaleXY"]
|
|
) {
|
|
view.layer.transform = finalTransform;
|
|
} else if ([property isEqualToString:@"opacity"]) {
|
|
view.layer.opacity = finalOpacity;
|
|
}
|
|
} withCompletionBlock:completion];
|
|
|
|
} else if (updatingLayoutAnimation) {
|
|
|
|
// Animate view update
|
|
[updatingLayoutAnimation performAnimations:^{
|
|
[view reactSetFrame:frame];
|
|
} withCompletionBlock:completion];
|
|
|
|
} else {
|
|
|
|
// Update without animation
|
|
[view reactSetFrame:frame];
|
|
completion(YES);
|
|
}
|
|
}
|
|
|
|
// Clean up
|
|
uiManager->_layoutAnimationGroup = nil;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A method to be called from JS, which takes a container ID and then releases
|
|
* all subviews for that container upon receipt.
|
|
*/
|
|
RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containerID)
|
|
{
|
|
id<RCTComponent> container = _shadowViewRegistry[containerID];
|
|
RCTAssert(container != nil, @"container view (for ID %@) not found", containerID);
|
|
|
|
NSUInteger subviewsCount = [container reactSubviews].count;
|
|
NSMutableArray<NSNumber *> *indices = [[NSMutableArray alloc] initWithCapacity:subviewsCount];
|
|
for (NSUInteger childIndex = 0; childIndex < subviewsCount; childIndex++) {
|
|
[indices addObject:@(childIndex)];
|
|
}
|
|
|
|
[self manageChildren:containerID
|
|
moveFromIndices:nil
|
|
moveToIndices:nil
|
|
addChildReactTags:nil
|
|
addAtIndices:nil
|
|
removeAtIndices:indices];
|
|
}
|
|
|
|
/**
|
|
* Disassociates children from container. Doesn't remove from registries.
|
|
* TODO: use [NSArray getObjects:buffer] to reuse same fast buffer each time.
|
|
*
|
|
* @returns Array of removed items.
|
|
*/
|
|
- (NSArray<id<RCTComponent>> *)_childrenToRemoveFromContainer:(id<RCTComponent>)container
|
|
atIndices:(NSArray<NSNumber *> *)atIndices
|
|
{
|
|
// If there are no indices to move or the container has no subviews don't bother
|
|
// We support parents with nil subviews so long as they're all nil so this allows for this behavior
|
|
if (atIndices.count == 0 || [container reactSubviews].count == 0) {
|
|
return nil;
|
|
}
|
|
// Construction of removed children must be done "up front", before indices are disturbed by removals.
|
|
NSMutableArray<id<RCTComponent>> *removedChildren = [NSMutableArray arrayWithCapacity:atIndices.count];
|
|
RCTAssert(container != nil, @"container view (for ID %@) not found", container);
|
|
for (NSNumber *indexNumber in atIndices) {
|
|
NSUInteger index = indexNumber.unsignedIntegerValue;
|
|
if (index < [container reactSubviews].count) {
|
|
[removedChildren addObject:[container reactSubviews][index]];
|
|
}
|
|
}
|
|
if (removedChildren.count != atIndices.count) {
|
|
NSString *message = [NSString stringWithFormat:@"removedChildren count (%tu) was not what we expected (%tu)",
|
|
removedChildren.count, atIndices.count];
|
|
RCTFatal(RCTErrorWithMessage(message));
|
|
}
|
|
return removedChildren;
|
|
}
|
|
|
|
- (void)_removeChildren:(NSArray<id<RCTComponent>> *)children
|
|
fromContainer:(id<RCTComponent>)container
|
|
{
|
|
for (id<RCTComponent> removedChild in children) {
|
|
[container removeReactSubview:removedChild];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove subviews from their parent with an animation.
|
|
*/
|
|
- (void)_removeChildren:(NSArray<UIView *> *)children
|
|
fromContainer:(UIView *)container
|
|
withAnimation:(RCTLayoutAnimationGroup *)animation
|
|
{
|
|
RCTAssertMainQueue();
|
|
RCTLayoutAnimation *deletingLayoutAnimation = animation.deletingLayoutAnimation;
|
|
|
|
__block NSUInteger completionsCalled = 0;
|
|
for (UIView *removedChild in children) {
|
|
|
|
void (^completion)(BOOL) = ^(BOOL finished) {
|
|
completionsCalled++;
|
|
|
|
[removedChild removeFromSuperview];
|
|
|
|
if (animation.callback && completionsCalled == children.count) {
|
|
animation.callback(@[@(finished)]);
|
|
|
|
// It's unsafe to call this callback more than once, so we nil it out here
|
|
// to make sure that doesn't happen.
|
|
animation.callback = nil;
|
|
}
|
|
};
|
|
|
|
// Hack: At this moment we have two contradict intents.
|
|
// First one: We want to delete the view from view hierarchy.
|
|
// Second one: We want to animate this view, which implies the existence of this view in the hierarchy.
|
|
// So, we have to remove this view from React's view hierarchy but postpone removing from UIKit's hierarchy.
|
|
// Here the problem: the default implementation of `-[UIView removeReactSubview:]` also removes the view from UIKit's hierarchy.
|
|
// So, let's temporary restore the view back after removing.
|
|
// To do so, we have to memorize original `superview` (which can differ from `container`) and an index of removed view.
|
|
UIView *originalSuperview = removedChild.superview;
|
|
NSUInteger originalIndex = [originalSuperview.subviews indexOfObjectIdenticalTo:removedChild];
|
|
[container removeReactSubview:removedChild];
|
|
// Disable user interaction while the view is animating
|
|
// since the view is (conceptually) deleted and not supposed to be interactive.
|
|
removedChild.userInteractionEnabled = NO;
|
|
[originalSuperview insertSubview:removedChild atIndex:originalIndex];
|
|
|
|
NSString *property = deletingLayoutAnimation.property;
|
|
[deletingLayoutAnimation performAnimations:^{
|
|
if ([property isEqualToString:@"scaleXY"]) {
|
|
removedChild.layer.transform = CATransform3DMakeScale(0.001, 0.001, 0.001);
|
|
} else if ([property isEqualToString:@"scaleX"]) {
|
|
removedChild.layer.transform = CATransform3DMakeScale(0.001, 1, 0.001);
|
|
} else if ([property isEqualToString:@"scaleY"]) {
|
|
removedChild.layer.transform = CATransform3DMakeScale(1, 0.001, 0.001);
|
|
} else if ([property isEqualToString:@"opacity"]) {
|
|
removedChild.layer.opacity = 0.0;
|
|
} else {
|
|
RCTLogError(@"Unsupported layout animation createConfig property %@",
|
|
deletingLayoutAnimation.property);
|
|
}
|
|
} withCompletionBlock:completion];
|
|
}
|
|
}
|
|
|
|
|
|
RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag)
|
|
{
|
|
RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag];
|
|
RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag);
|
|
[self _purgeChildren:(NSArray<id<RCTComponent>> *)rootShadowView.reactSubviews
|
|
fromRegistry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry];
|
|
[_shadowViewRegistry removeObjectForKey:rootReactTag];
|
|
[_rootViewTags removeObject:rootReactTag];
|
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){
|
|
RCTAssertMainQueue();
|
|
UIView *rootView = viewRegistry[rootReactTag];
|
|
[uiManager _purgeChildren:(NSArray<id<RCTComponent>> *)rootView.reactSubviews
|
|
fromRegistry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry];
|
|
[(NSMutableDictionary *)viewRegistry removeObjectForKey:rootReactTag];
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag
|
|
withView:(nonnull NSNumber *)newReactTag)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
RCTAssert(shadowView != nil, @"shadowView (for ID %@) not found", reactTag);
|
|
|
|
RCTShadowView *superShadowView = shadowView.superview;
|
|
if (!superShadowView) {
|
|
RCTAssert(NO, @"shadowView super (of ID %@) not found", reactTag);
|
|
return;
|
|
}
|
|
|
|
NSUInteger indexOfView = [superShadowView.reactSubviews indexOfObjectIdenticalTo:shadowView];
|
|
RCTAssert(indexOfView != NSNotFound, @"View's superview doesn't claim it as subview (id %@)", reactTag);
|
|
NSArray<NSNumber *> *removeAtIndices = @[@(indexOfView)];
|
|
NSArray<NSNumber *> *addTags = @[newReactTag];
|
|
[self manageChildren:superShadowView.reactTag
|
|
moveFromIndices:nil
|
|
moveToIndices:nil
|
|
addChildReactTags:addTags
|
|
addAtIndices:removeAtIndices
|
|
removeAtIndices:removeAtIndices];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(setChildren:(nonnull NSNumber *)containerTag
|
|
reactTags:(NSArray<NSNumber *> *)reactTags)
|
|
{
|
|
RCTSetChildren(containerTag, reactTags,
|
|
(NSDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry);
|
|
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){
|
|
|
|
RCTSetChildren(containerTag, reactTags,
|
|
(NSDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry);
|
|
}];
|
|
|
|
[self _shadowViewDidReceiveUpdatedChildren:_shadowViewRegistry[containerTag]];
|
|
}
|
|
|
|
static void RCTSetChildren(NSNumber *containerTag,
|
|
NSArray<NSNumber *> *reactTags,
|
|
NSDictionary<NSNumber *, id<RCTComponent>> *registry)
|
|
{
|
|
id<RCTComponent> container = registry[containerTag];
|
|
NSInteger index = 0;
|
|
for (NSNumber *reactTag in reactTags) {
|
|
id<RCTComponent> view = registry[reactTag];
|
|
if (view) {
|
|
[container insertReactSubview:view atIndex:index++];
|
|
}
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
|
|
moveFromIndices:(NSArray<NSNumber *> *)moveFromIndices
|
|
moveToIndices:(NSArray<NSNumber *> *)moveToIndices
|
|
addChildReactTags:(NSArray<NSNumber *> *)addChildReactTags
|
|
addAtIndices:(NSArray<NSNumber *> *)addAtIndices
|
|
removeAtIndices:(NSArray<NSNumber *> *)removeAtIndices)
|
|
{
|
|
[self _manageChildren:containerTag
|
|
moveFromIndices:moveFromIndices
|
|
moveToIndices:moveToIndices
|
|
addChildReactTags:addChildReactTags
|
|
addAtIndices:addAtIndices
|
|
removeAtIndices:removeAtIndices
|
|
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry];
|
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){
|
|
[uiManager _manageChildren:containerTag
|
|
moveFromIndices:moveFromIndices
|
|
moveToIndices:moveToIndices
|
|
addChildReactTags:addChildReactTags
|
|
addAtIndices:addAtIndices
|
|
removeAtIndices:removeAtIndices
|
|
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry];
|
|
}];
|
|
|
|
[self _shadowViewDidReceiveUpdatedChildren:_shadowViewRegistry[containerTag]];
|
|
}
|
|
|
|
- (void)_manageChildren:(NSNumber *)containerTag
|
|
moveFromIndices:(NSArray<NSNumber *> *)moveFromIndices
|
|
moveToIndices:(NSArray<NSNumber *> *)moveToIndices
|
|
addChildReactTags:(NSArray<NSNumber *> *)addChildReactTags
|
|
addAtIndices:(NSArray<NSNumber *> *)addAtIndices
|
|
removeAtIndices:(NSArray<NSNumber *> *)removeAtIndices
|
|
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)registry
|
|
{
|
|
id<RCTComponent> container = registry[containerTag];
|
|
RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count);
|
|
RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add");
|
|
|
|
// Removes (both permanent and temporary moves) are using "before" indices
|
|
NSArray<id<RCTComponent>> *permanentlyRemovedChildren =
|
|
[self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
|
|
NSArray<id<RCTComponent>> *temporarilyRemovedChildren =
|
|
[self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
|
|
|
|
BOOL isUIViewRegistry = ((id)registry == (id)_viewRegistry);
|
|
if (isUIViewRegistry && _layoutAnimationGroup.deletingLayoutAnimation) {
|
|
[self _removeChildren:(NSArray<UIView *> *)permanentlyRemovedChildren
|
|
fromContainer:(UIView *)container
|
|
withAnimation:_layoutAnimationGroup];
|
|
} else {
|
|
[self _removeChildren:permanentlyRemovedChildren fromContainer:container];
|
|
}
|
|
|
|
[self _removeChildren:temporarilyRemovedChildren fromContainer:container];
|
|
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
|
|
|
|
// Figure out what to insert - merge temporary inserts and adds
|
|
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
|
|
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
|
|
destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index];
|
|
}
|
|
|
|
for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) {
|
|
id<RCTComponent> view = registry[addChildReactTags[index]];
|
|
if (view) {
|
|
destinationsToChildrenToAdd[addAtIndices[index]] = view;
|
|
}
|
|
}
|
|
|
|
NSArray<NSNumber *> *sortedIndices =
|
|
[destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
|
for (NSNumber *reactIndex in sortedIndices) {
|
|
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex]
|
|
atIndex:reactIndex.integerValue];
|
|
}
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
|
|
viewName:(NSString *)viewName
|
|
rootTag:(nonnull NSNumber *)rootTag
|
|
props:(NSDictionary *)props)
|
|
{
|
|
RCTComponentData *componentData = _componentDataByName[viewName];
|
|
if (componentData == nil) {
|
|
RCTLogError(@"No component found for view with name \"%@\"", viewName);
|
|
}
|
|
|
|
// Register shadow view
|
|
RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag];
|
|
if (shadowView) {
|
|
[componentData setProps:props forShadowView:shadowView];
|
|
_shadowViewRegistry[reactTag] = shadowView;
|
|
RCTShadowView *rootView = _shadowViewRegistry[rootTag];
|
|
RCTAssert([rootView isKindOfClass:[RCTRootShadowView class]] ||
|
|
[rootView isKindOfClass:[RCTSurfaceRootShadowView class]],
|
|
@"Given `rootTag` (%@) does not correspond to a valid root shadow view instance.", rootTag);
|
|
shadowView.rootView = (RCTRootShadowView *)rootView;
|
|
}
|
|
|
|
// Dispatch view creation directly to the main thread instead of adding to
|
|
// UIBlocks array. This way, it doesn't get deferred until after layout.
|
|
__block UIView *preliminaryCreatedView = nil;
|
|
|
|
void (^createViewBlock)(void) = ^{
|
|
// Do nothing on the second run.
|
|
if (preliminaryCreatedView) {
|
|
return;
|
|
}
|
|
|
|
preliminaryCreatedView = [componentData createViewWithTag:reactTag];
|
|
|
|
if (preliminaryCreatedView) {
|
|
self->_viewRegistry[reactTag] = preliminaryCreatedView;
|
|
}
|
|
};
|
|
|
|
// We cannot guarantee that asynchronously scheduled block will be executed
|
|
// *before* a block is added to the regular mounting process (simply because
|
|
// mounting process can be managed externally while the main queue is
|
|
// locked).
|
|
// So, we positively dispatch it asynchronously and double check inside
|
|
// the regular mounting block.
|
|
|
|
RCTExecuteOnMainQueue(createViewBlock);
|
|
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
createViewBlock();
|
|
|
|
if (preliminaryCreatedView) {
|
|
[componentData setProps:props forView:preliminaryCreatedView];
|
|
}
|
|
}];
|
|
|
|
[self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(updateView:(nonnull NSNumber *)reactTag
|
|
viewName:(NSString *)viewName // not always reliable, use shadowView.viewName if available
|
|
props:(NSDictionary *)props)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
RCTComponentData *componentData = _componentDataByName[shadowView.viewName ?: viewName];
|
|
[componentData setProps:props forShadowView:shadowView];
|
|
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
UIView *view = viewRegistry[reactTag];
|
|
[componentData setProps:props forView:view];
|
|
}];
|
|
|
|
[self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]];
|
|
}
|
|
|
|
- (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag
|
|
viewName:(NSString *)viewName
|
|
props:(NSDictionary *)props
|
|
{
|
|
RCTAssertMainQueue();
|
|
RCTComponentData *componentData = _componentDataByName[viewName];
|
|
UIView *view = _viewRegistry[reactTag];
|
|
[componentData setProps:props forView:view];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
UIView *newResponder = viewRegistry[reactTag];
|
|
[newResponder reactFocus];
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(blur:(nonnull NSNumber *)reactTag)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){
|
|
UIView *currentResponder = viewRegistry[reactTag];
|
|
[currentResponder reactBlur];
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)point callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
UIView *view = viewRegistry[reactTag];
|
|
UIView *target = [view hitTest:point withEvent:nil];
|
|
CGRect frame = [target convertRect:target.bounds toView:view];
|
|
|
|
while (target.reactTag == nil && target.superview != nil) {
|
|
target = target.superview;
|
|
}
|
|
|
|
callback(@[
|
|
RCTNullIfNil(target.reactTag),
|
|
@(frame.origin.x),
|
|
@(frame.origin.y),
|
|
@(frame.size.width),
|
|
@(frame.size.height),
|
|
]);
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag
|
|
commandID:(NSInteger)commandID
|
|
commandArgs:(NSArray<id> *)commandArgs)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
RCTComponentData *componentData = _componentDataByName[shadowView.viewName];
|
|
Class managerClass = componentData.managerClass;
|
|
RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)];
|
|
id<RCTBridgeMethod> method = moduleData.methods[commandID];
|
|
|
|
NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs];
|
|
[method invokeWithBridge:_bridge module:componentData.manager arguments:args];
|
|
}
|
|
|
|
- (void)batchDidComplete
|
|
{
|
|
[self _layoutAndMount];
|
|
}
|
|
|
|
/**
|
|
* Sets up animations, computes layout, creates UI mounting blocks for computed layout,
|
|
* runs these blocks and all other already existing blocks.
|
|
*/
|
|
- (void)_layoutAndMount
|
|
{
|
|
[self _dispatchPropsDidChangeEvents];
|
|
[self _dispatchChildrenDidChangeEvents];
|
|
|
|
[_observerCoordinator uiManagerWillPerformLayout:self];
|
|
|
|
// Perform layout
|
|
for (NSNumber *reactTag in _rootViewTags) {
|
|
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
|
|
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
|
|
}
|
|
|
|
[_observerCoordinator uiManagerDidPerformLayout:self];
|
|
|
|
[_observerCoordinator uiManagerWillPerformMounting:self];
|
|
|
|
[self flushUIBlocksWithCompletion:^{
|
|
[self->_observerCoordinator uiManagerDidPerformMounting:self];
|
|
}];
|
|
}
|
|
|
|
- (void)flushUIBlocksWithCompletion:(void (^)(void))completion;
|
|
{
|
|
RCTAssertUIManagerQueue();
|
|
|
|
// First copy the previous blocks into a temporary variable, then reset the
|
|
// pending blocks to a new array. This guards against mutation while
|
|
// processing the pending blocks in another thread.
|
|
NSArray<RCTViewManagerUIBlock> *previousPendingUIBlocks = _pendingUIBlocks;
|
|
_pendingUIBlocks = [NSMutableArray new];
|
|
|
|
if (previousPendingUIBlocks.count == 0) {
|
|
completion();
|
|
return;
|
|
}
|
|
|
|
__weak typeof(self) weakSelf = self;
|
|
|
|
void (^mountingBlock)(void) = ^{
|
|
typeof(self) strongSelf = weakSelf;
|
|
|
|
@try {
|
|
for (RCTViewManagerUIBlock block in previousPendingUIBlocks) {
|
|
block(strongSelf, strongSelf->_viewRegistry);
|
|
}
|
|
}
|
|
@catch (NSException *exception) {
|
|
RCTLogError(@"Exception thrown while executing UI block: %@", exception);
|
|
}
|
|
};
|
|
|
|
if ([self.observerCoordinator uiManager:self performMountingWithBlock:mountingBlock]) {
|
|
completion();
|
|
return;
|
|
}
|
|
|
|
// Execute the previously queued UI blocks
|
|
RCTProfileBeginFlowEvent();
|
|
RCTExecuteOnMainQueue(^{
|
|
RCTProfileEndFlowEvent();
|
|
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[UIManager flushUIBlocks]", (@{
|
|
@"count": [@(previousPendingUIBlocks.count) stringValue],
|
|
}));
|
|
|
|
mountingBlock();
|
|
|
|
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
|
|
|
|
RCTExecuteOnUIManagerQueue(completion);
|
|
});
|
|
}
|
|
|
|
- (void)setNeedsLayout
|
|
{
|
|
// If there is an active batch layout will happen when batch finished, so we will wait for that.
|
|
// Otherwise we immediately trigger layout.
|
|
if (![_bridge isBatchActive] && ![_bridge isLoading]) {
|
|
[self _layoutAndMount];
|
|
}
|
|
}
|
|
|
|
- (void)_shadowView:(RCTShadowView *)shadowView didReceiveUpdatedProps:(NSArray<NSString *> *)props
|
|
{
|
|
// We collect a set with changed `shadowViews` and its changed props,
|
|
// so we have to maintain this collection properly.
|
|
NSArray<NSString *> *previousProps;
|
|
if ((previousProps = [_shadowViewsWithUpdatedProps objectForKey:shadowView])) {
|
|
// Merging already registred changed props and new ones.
|
|
NSMutableSet *set = [NSMutableSet setWithArray:previousProps];
|
|
[set addObjectsFromArray:props];
|
|
props = [set allObjects];
|
|
}
|
|
|
|
[_shadowViewsWithUpdatedProps setObject:props forKey:shadowView];
|
|
}
|
|
|
|
- (void)_shadowViewDidReceiveUpdatedChildren:(RCTShadowView *)shadowView
|
|
{
|
|
[_shadowViewsWithUpdatedChildren addObject:shadowView];
|
|
}
|
|
|
|
- (void)_dispatchChildrenDidChangeEvents
|
|
{
|
|
if (_shadowViewsWithUpdatedChildren.count == 0) {
|
|
return;
|
|
}
|
|
|
|
NSHashTable<RCTShadowView *> *shadowViews = _shadowViewsWithUpdatedChildren;
|
|
_shadowViewsWithUpdatedChildren = [NSHashTable weakObjectsHashTable];
|
|
|
|
NSMutableArray *tags = [NSMutableArray arrayWithCapacity:shadowViews.count];
|
|
|
|
for (RCTShadowView *shadowView in shadowViews) {
|
|
[shadowView didUpdateReactSubviews];
|
|
[tags addObject:shadowView.reactTag];
|
|
}
|
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
for (NSNumber *tag in tags) {
|
|
UIView<RCTComponent> *view = viewRegistry[tag];
|
|
[view didUpdateReactSubviews];
|
|
}
|
|
}];
|
|
}
|
|
|
|
- (void)_dispatchPropsDidChangeEvents
|
|
{
|
|
if (_shadowViewsWithUpdatedProps.count == 0) {
|
|
return;
|
|
}
|
|
|
|
NSMapTable<RCTShadowView *, NSArray<NSString *> *> *shadowViews = _shadowViewsWithUpdatedProps;
|
|
_shadowViewsWithUpdatedProps = [NSMapTable weakToStrongObjectsMapTable];
|
|
|
|
NSMapTable<NSNumber *, NSArray<NSString *> *> *tags = [NSMapTable strongToStrongObjectsMapTable];
|
|
|
|
for (RCTShadowView *shadowView in shadowViews) {
|
|
NSArray<NSString *> *props = [shadowViews objectForKey:shadowView];
|
|
[shadowView didSetProps:props];
|
|
[tags setObject:props forKey:shadowView.reactTag];
|
|
}
|
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
for (NSNumber *tag in tags) {
|
|
UIView<RCTComponent> *view = viewRegistry[tag];
|
|
[view didSetProps:[tags objectForKey:tag]];
|
|
}
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(measure:(nonnull NSNumber *)reactTag
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
UIView *view = viewRegistry[reactTag];
|
|
if (!view) {
|
|
// this view was probably collapsed out
|
|
RCTLogWarn(@"measure cannot find view with tag #%@", reactTag);
|
|
callback(@[]);
|
|
return;
|
|
}
|
|
|
|
// If in a <Modal>, rootView will be the root of the modal container.
|
|
UIView *rootView = view;
|
|
while (rootView.superview && ![rootView isReactRootView]) {
|
|
rootView = rootView.superview;
|
|
}
|
|
|
|
// By convention, all coordinates, whether they be touch coordinates, or
|
|
// measurement coordinates are with respect to the root view.
|
|
CGRect frame = view.frame;
|
|
CGRect globalBounds = [view convertRect:view.bounds toView:rootView];
|
|
|
|
callback(@[
|
|
@(frame.origin.x),
|
|
@(frame.origin.y),
|
|
@(globalBounds.size.width),
|
|
@(globalBounds.size.height),
|
|
@(globalBounds.origin.x),
|
|
@(globalBounds.origin.y),
|
|
]);
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(measureInWindow:(nonnull NSNumber *)reactTag
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
UIView *view = viewRegistry[reactTag];
|
|
if (!view) {
|
|
// this view was probably collapsed out
|
|
RCTLogWarn(@"measure cannot find view with tag #%@", reactTag);
|
|
callback(@[]);
|
|
return;
|
|
}
|
|
|
|
// Return frame coordinates in window
|
|
CGRect windowFrame = [view.window convertRect:view.frame fromView:view.superview];
|
|
callback(@[
|
|
@(windowFrame.origin.x),
|
|
@(windowFrame.origin.y),
|
|
@(windowFrame.size.width),
|
|
@(windowFrame.size.height),
|
|
]);
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Returs if the shadow view provided has the `ancestor` shadow view as
|
|
* an actual ancestor.
|
|
*/
|
|
RCT_EXPORT_METHOD(viewIsDescendantOf:(nonnull NSNumber *)reactTag
|
|
ancestor:(nonnull NSNumber *)ancestorReactTag
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag];
|
|
if (!shadowView) {
|
|
return;
|
|
}
|
|
if (!ancestorShadowView) {
|
|
return;
|
|
}
|
|
BOOL viewIsAncestor = [shadowView viewIsDescendantOf:ancestorShadowView];
|
|
callback(@[@(viewIsAncestor)]);
|
|
}
|
|
|
|
static void RCTMeasureLayout(RCTShadowView *view,
|
|
RCTShadowView *ancestor,
|
|
RCTResponseSenderBlock callback)
|
|
{
|
|
if (!view) {
|
|
return;
|
|
}
|
|
if (!ancestor) {
|
|
return;
|
|
}
|
|
CGRect result = [view measureLayoutRelativeToAncestor:ancestor];
|
|
if (CGRectIsNull(result)) {
|
|
RCTLogError(@"view %@ (tag #%@) is not a descendant of %@ (tag #%@)",
|
|
view, view.reactTag, ancestor, ancestor.reactTag);
|
|
return;
|
|
}
|
|
CGFloat leftOffset = result.origin.x;
|
|
CGFloat topOffset = result.origin.y;
|
|
CGFloat width = result.size.width;
|
|
CGFloat height = result.size.height;
|
|
if (isnan(leftOffset) || isnan(topOffset) || isnan(width) || isnan(height)) {
|
|
RCTLogError(@"Attempted to measure layout but offset or dimensions were NaN");
|
|
return;
|
|
}
|
|
callback(@[@(leftOffset), @(topOffset), @(width), @(height)]);
|
|
}
|
|
|
|
/**
|
|
* Returns the computed recursive offset layout in a dictionary form. The
|
|
* returned values are relative to the `ancestor` shadow view. Returns `nil`, if
|
|
* the `ancestor` shadow view is not actually an `ancestor`. Does not touch
|
|
* anything on the main UI thread. Invokes supplied callback with (x, y, width,
|
|
* height).
|
|
*/
|
|
RCT_EXPORT_METHOD(measureLayout:(nonnull NSNumber *)reactTag
|
|
relativeTo:(nonnull NSNumber *)ancestorReactTag
|
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag];
|
|
RCTMeasureLayout(shadowView, ancestorShadowView, callback);
|
|
}
|
|
|
|
/**
|
|
* Returns the computed recursive offset layout in a dictionary form. The
|
|
* returned values are relative to the `ancestor` shadow view. Returns `nil`, if
|
|
* the `ancestor` shadow view is not actually an `ancestor`. Does not touch
|
|
* anything on the main UI thread. Invokes supplied callback with (x, y, width,
|
|
* height).
|
|
*/
|
|
RCT_EXPORT_METHOD(measureLayoutRelativeToParent:(nonnull NSNumber *)reactTag
|
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
RCTMeasureLayout(shadowView, shadowView.reactSuperview, callback);
|
|
}
|
|
|
|
/**
|
|
* Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews
|
|
* that are immediate descendants to the parent view found within a specified rect. The dictionary result
|
|
* contains left, top, width, height and an index. The index specifies the position among the other subviews.
|
|
* Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the
|
|
* passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts.
|
|
*/
|
|
RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect
|
|
parentView:(nonnull NSNumber *)reactTag
|
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback
|
|
callback:(RCTResponseSenderBlock)callback)
|
|
{
|
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
|
|
if (!shadowView) {
|
|
RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag);
|
|
return;
|
|
}
|
|
NSArray<RCTShadowView *> *childShadowViews = [shadowView reactSubviews];
|
|
NSMutableArray<NSDictionary *> *results =
|
|
[[NSMutableArray alloc] initWithCapacity:childShadowViews.count];
|
|
|
|
[childShadowViews enumerateObjectsUsingBlock:
|
|
^(RCTShadowView *childShadowView, NSUInteger idx, __unused BOOL *stop) {
|
|
CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView];
|
|
if (CGRectIsNull(childLayout)) {
|
|
RCTLogError(@"View %@ (tag #%@) is not a descendant of %@ (tag #%@)",
|
|
childShadowView, childShadowView.reactTag, shadowView, shadowView.reactTag);
|
|
return;
|
|
}
|
|
|
|
CGFloat leftOffset = childLayout.origin.x;
|
|
CGFloat topOffset = childLayout.origin.y;
|
|
CGFloat width = childLayout.size.width;
|
|
CGFloat height = childLayout.size.height;
|
|
|
|
if (leftOffset <= rect.origin.x + rect.size.width &&
|
|
leftOffset + width >= rect.origin.x &&
|
|
topOffset <= rect.origin.y + rect.size.height &&
|
|
topOffset + height >= rect.origin.y) {
|
|
|
|
// This view is within the layout rect
|
|
NSDictionary *result = @{@"index": @(idx),
|
|
@"left": @(leftOffset),
|
|
@"top": @(topOffset),
|
|
@"width": @(width),
|
|
@"height": @(height)};
|
|
|
|
[results addObject:result];
|
|
}
|
|
}];
|
|
callback(@[results]);
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(takeSnapshot:(id /* NSString or NSNumber */)target
|
|
withOptions:(NSDictionary *)options
|
|
resolve:(RCTPromiseResolveBlock)resolve
|
|
reject:(RCTPromiseRejectBlock)reject)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
|
|
// Get view
|
|
UIView *view;
|
|
if (target == nil || [target isEqual:@"window"]) {
|
|
view = RCTKeyWindow();
|
|
} else if ([target isKindOfClass:[NSNumber class]]) {
|
|
view = viewRegistry[target];
|
|
if (!view) {
|
|
RCTLogError(@"No view found with reactTag: %@", target);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Get options
|
|
CGSize size = [RCTConvert CGSize:options];
|
|
NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"];
|
|
|
|
// Capture image
|
|
if (size.width < 0.1 || size.height < 0.1) {
|
|
size = view.bounds.size;
|
|
}
|
|
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
|
BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES];
|
|
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
|
UIGraphicsEndImageContext();
|
|
|
|
if (!success || !image) {
|
|
reject(RCTErrorUnspecified, @"Failed to capture view snapshot.", nil);
|
|
return;
|
|
}
|
|
|
|
// Convert image to data (on a background thread)
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
|
|
NSData *data;
|
|
if ([format isEqualToString:@"png"]) {
|
|
data = UIImagePNGRepresentation(image);
|
|
} else if ([format isEqualToString:@"jpeg"]) {
|
|
CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1];
|
|
data = UIImageJPEGRepresentation(image, quality);
|
|
} else {
|
|
RCTLogError(@"Unsupported image format: %@", format);
|
|
return;
|
|
}
|
|
|
|
// Save to a temp file
|
|
NSError *error = nil;
|
|
NSString *tempFilePath = RCTTempFilePath(format, &error);
|
|
if (tempFilePath) {
|
|
if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) {
|
|
resolve(tempFilePath);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we reached here, something went wrong
|
|
reject(RCTErrorUnspecified, error.localizedDescription, error);
|
|
});
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* JS sets what *it* considers to be the responder. Later, scroll views can use
|
|
* this in order to determine if scrolling is appropriate.
|
|
*/
|
|
RCT_EXPORT_METHOD(setJSResponder:(nonnull NSNumber *)reactTag
|
|
blockNativeResponder:(__unused BOOL)blockNativeResponder)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
_jsResponder = viewRegistry[reactTag];
|
|
if (!_jsResponder) {
|
|
RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag);
|
|
}
|
|
}];
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(clearJSResponder)
|
|
{
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
_jsResponder = nil;
|
|
}];
|
|
}
|
|
|
|
- (NSDictionary<NSString *, id> *)constantsToExport
|
|
{
|
|
NSMutableDictionary<NSString *, NSDictionary *> *constants = [NSMutableDictionary new];
|
|
NSMutableDictionary<NSString *, NSDictionary *> *directEvents = [NSMutableDictionary new];
|
|
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEvents = [NSMutableDictionary new];
|
|
|
|
[_componentDataByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) {
|
|
NSMutableDictionary<NSString *, id> *moduleConstants = [NSMutableDictionary new];
|
|
|
|
// Register which event-types this view dispatches.
|
|
// React needs this for the event plugin.
|
|
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEventTypes = [NSMutableDictionary new];
|
|
NSMutableDictionary<NSString *, NSDictionary *> *directEventTypes = [NSMutableDictionary new];
|
|
|
|
// Add manager class
|
|
moduleConstants[@"Manager"] = RCTBridgeModuleNameForClass(componentData.managerClass);
|
|
|
|
// Add native props
|
|
NSDictionary<NSString *, id> *viewConfig = [componentData viewConfig];
|
|
moduleConstants[@"NativeProps"] = viewConfig[@"propTypes"];
|
|
moduleConstants[@"baseModuleName"] = viewConfig[@"baseModuleName"];
|
|
moduleConstants[@"bubblingEventTypes"] = bubblingEventTypes;
|
|
moduleConstants[@"directEventTypes"] = directEventTypes;
|
|
|
|
// Add direct events
|
|
for (NSString *eventName in viewConfig[@"directEvents"]) {
|
|
if (!directEvents[eventName]) {
|
|
directEvents[eventName] = @{
|
|
@"registrationName": [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"],
|
|
};
|
|
}
|
|
directEventTypes[eventName] = directEvents[eventName];
|
|
if (RCT_DEBUG && bubblingEvents[eventName]) {
|
|
RCTLogError(@"Component '%@' re-registered bubbling event '%@' as a "
|
|
"direct event", componentData.name, eventName);
|
|
}
|
|
}
|
|
|
|
// Add bubbling events
|
|
for (NSString *eventName in viewConfig[@"bubblingEvents"]) {
|
|
if (!bubblingEvents[eventName]) {
|
|
NSString *bubbleName = [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"];
|
|
bubblingEvents[eventName] = @{
|
|
@"phasedRegistrationNames": @{
|
|
@"bubbled": bubbleName,
|
|
@"captured": [bubbleName stringByAppendingString:@"Capture"],
|
|
}
|
|
};
|
|
}
|
|
bubblingEventTypes[eventName] = bubblingEvents[eventName];
|
|
if (RCT_DEBUG && directEvents[eventName]) {
|
|
RCTLogError(@"Component '%@' re-registered direct event '%@' as a "
|
|
"bubbling event", componentData.name, eventName);
|
|
}
|
|
}
|
|
|
|
RCTAssert(!constants[name], @"UIManager already has constants for %@", componentData.name);
|
|
constants[name] = moduleConstants;
|
|
}];
|
|
|
|
return constants;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config
|
|
withCallback:(RCTResponseSenderBlock)callback
|
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback)
|
|
{
|
|
RCTLayoutAnimationGroup *layoutAnimationGroup =
|
|
[[RCTLayoutAnimationGroup alloc] initWithConfig:config
|
|
callback:callback];
|
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
|
[uiManager setNextLayoutAnimationGroup:layoutAnimationGroup];
|
|
}];
|
|
}
|
|
|
|
- (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView *view))completion
|
|
{
|
|
RCTAssertMainQueue();
|
|
RCTAssert(completion != nil, @"Attempted to resolve rootView for tag %@ without a completion block", reactTag);
|
|
|
|
if (reactTag == nil) {
|
|
completion(nil);
|
|
return;
|
|
}
|
|
|
|
RCTExecuteOnUIManagerQueue(^{
|
|
NSNumber *rootTag = [self shadowViewForReactTag:reactTag].rootView.reactTag;
|
|
RCTExecuteOnMainQueue(^{
|
|
UIView *rootView = nil;
|
|
if (rootTag != nil) {
|
|
rootView = [self viewForReactTag:rootTag];
|
|
}
|
|
completion(rootView);
|
|
});
|
|
});
|
|
}
|
|
|
|
static UIView *_jsResponder;
|
|
|
|
+ (UIView *)JSResponder
|
|
{
|
|
return _jsResponder;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation RCTBridge (RCTUIManager)
|
|
|
|
- (RCTUIManager *)uiManager
|
|
{
|
|
return [self moduleForClass:[RCTUIManager class]];
|
|
}
|
|
|
|
@end
|