react-native/ReactKit/Modules/RCTUIManager.m

1191 lines
43 KiB
Objective-C

// Copyright 2004-present Facebook. All Rights Reserved.
#import "RCTUIManager.h"
#import <AVFoundation/AVFoundation.h>
#import <objc/message.h>
#import <pthread/pthread.h>
#import "Layout.h"
#import "RCTAssert.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTRootView.h"
#import "RCTLog.h"
#import "RCTNavigator.h"
#import "RCTScrollableProtocol.h"
#import "RCTShadowView.h"
#import "RCTSparseArray.h"
#import "RCTUtils.h"
#import "RCTView.h"
#import "RCTViewNodeProtocol.h"
#import "RCTUIViewManager.h"
#import "UIView+ReactKit.h"
@class RCTAnimationConfig;
typedef void (^react_view_node_block_t)(id<RCTViewNodeProtocol>);
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, react_view_node_block_t block) {
if (view.reactTag) block(view);
for (id<RCTViewNodeProtocol> subview in view.reactSubviews) {
RCTTraverseViewNodes(subview, block);
}
}
@implementation RCTUIManager
{
// Root views are only mutated on the shadow queue
NSDictionary *_viewManagers;
NSMutableSet *_rootViewTags;
NSMutableArray *_pendingUIBlocks;
pthread_mutex_t _pendingUIBlocksMutex;
dispatch_queue_t _shadowQueue;
NSDictionary *_nextLayoutAnimationConfig; // RCT thread only
RCTResponseSenderBlock _nextLayoutAnimationCallback; // RCT thread only
RCTResponseSenderBlock _layoutAnimationCallbackMT; // Main thread only
NSMutableDictionary *_defaultShadowViews;
NSMutableDictionary *_defaultViews;
}
- (id <RCTNativeViewModule>)_managerInstanceForViewWithModuleName:(NSString *)moduleName
{
id <RCTNativeViewModule> managerInstance = _viewManagers[moduleName];
if (managerInstance == nil) {
RCTLogWarn(@"No manager class found for view with module name \"%@\"", moduleName);
managerInstance = [[RCTUIViewManager alloc] init];
}
return managerInstance;
}
- (instancetype)initWithShadowQueue:(dispatch_queue_t)shadowQueue
viewManagers:(NSDictionary *)viewManagers
{
if ((self = [super init])) {
_viewManagers = viewManagers;
_viewRegistry = [[RCTSparseArray alloc] init];
_shadowViewRegistry = [[RCTSparseArray alloc] init];
_shadowQueue = shadowQueue;
pthread_mutex_init(&_pendingUIBlocksMutex, NULL);
// Internal resources
_pendingUIBlocks = [[NSMutableArray alloc] init];
_rootViewTags = [[NSMutableSet alloc] init];
_defaultShadowViews = [[NSMutableDictionary alloc] init];
_defaultViews = [[NSMutableDictionary alloc] init];
}
return self;
}
- (instancetype)init
{
RCT_NOT_DESIGNATED_INITIALIZER();
}
- (void)dealloc
{
RCTAssert(!self.valid, @"must call -invalidate before -dealloc");
pthread_mutex_destroy(&_pendingUIBlocksMutex);
}
- (BOOL)isValid
{
return _viewRegistry != nil;
}
- (void)invalidate
{
_viewRegistry = nil;
_shadowViewRegistry = nil;
pthread_mutex_lock(&_pendingUIBlocksMutex);
_pendingUIBlocks = nil;
pthread_mutex_unlock(&_pendingUIBlocksMutex);
}
- (void)registerRootView:(RCTRootView *)rootView;
{
NSNumber *reactTag = rootView.reactTag;
UIView *existingView = _viewRegistry[reactTag];
RCTCAssert(existingView == nil || existingView == rootView,
@"Expect all root views to have unique tag. Added %@ twice", reactTag);
// Register view
_viewRegistry[reactTag] = rootView;
CGRect frame = rootView.frame;
// Register shadow view
dispatch_async(_shadowQueue, ^{
RCTShadowView *shadowView = [[RCTShadowView alloc] init];
shadowView.reactTag = reactTag;
shadowView.frame = frame;
shadowView.backgroundColor = [UIColor whiteColor];
shadowView.reactRootView = YES; // can this just be inferred from the fact that it has no superview?
_shadowViewRegistry[shadowView.reactTag] = shadowView;
[_rootViewTags addObject:reactTag];
});
}
+ (UIView *)closestReactAncestor:(UIView *)view
{
UIView *currentUIView = view;
while (currentUIView && !currentUIView.reactTag) {
currentUIView = currentUIView.superview;
}
return (UIView *)currentUIView;
}
+ (UIView *)closestReactAncestorThatRespondsToTouch:(UITouch *)touch
{
UIView *currentUIView = [RCTUIManager closestReactAncestor:touch.view];
while (currentUIView != nil) {
if ([currentUIView isUserInteractionEnabled]) { // TODO: implement respondsToTouch:touch mechanism
return currentUIView;
}
currentUIView = [RCTUIManager closestReactAncestor:currentUIView.superview];
}
return nil;
}
/**
* Unregisters views from registries
*/
- (void)_purgeChildren:(NSArray *)children fromRegistry:(RCTSparseArray *)registry
{
for (id<RCTViewNodeProtocol> child in children) {
RCTTraverseViewNodes(registry[child.reactTag], ^(id<RCTViewNodeProtocol> subview) {
RCTAssert(![subview isReactRootView], @"Host views should not be unregistered");
if ([subview respondsToSelector:@selector(reactWillDestroy)]) {
[subview reactWillDestroy];
}
registry[subview.reactTag] = nil;
});
}
}
- (void)addUIBlock:(RCTViewManagerUIBlock)block
{
// This assert is fragile. This is temporary pending t4698600
RCTAssert(![NSThread isMainThread], @"This method should only be called on the shadow thread");
__weak RCTUIManager *weakViewManager = self;
__weak RCTSparseArray *weakViewRegistry = _viewRegistry;
dispatch_block_t outerBlock = ^{
RCTUIManager *strongViewManager = weakViewManager;
RCTSparseArray *strongViewRegistry = weakViewRegistry;
if (strongViewManager && strongViewRegistry) {
block(strongViewManager, strongViewRegistry);
}
};
pthread_mutex_lock(&_pendingUIBlocksMutex);
[_pendingUIBlocks addObject:[outerBlock copy]];
pthread_mutex_unlock(&_pendingUIBlocksMutex);
}
- (void)setViewLayout:(UIView *)view withAnchorPoint:(CGPoint)anchorPoint position:(CGPoint)position bounds:(CGRect)bounds config:(RCTAnimationConfig *)config completion:(void (^)(BOOL finished))completion
{
if (isnan(position.x) || isnan(position.y) ||
isnan(bounds.origin.x) || isnan(bounds.origin.y) ||
isnan(bounds.size.width) || isnan(bounds.size.height)) {
RCTLogError(@"Invalid layout for (%zd)%@. position: %@. bounds: %@", [view reactTag], self, NSStringFromCGPoint(position), NSStringFromCGRect(bounds));
return;
}
view.layer.anchorPoint = anchorPoint;
view.layer.position = position;
view.layer.bounds = bounds;
completion(YES);
}
/**
* TODO: `RCTBridge` has first class knowledge of this method. We should either:
* 1. Require that the JS trigger this after a batch - almost like a flush.
* 2. Build in support to the `<BatchedExports>` protocol so that each module
* may return values to JS via a third callback function passed in, but can
* return a tuple that is `(UIThreadBlocks, JSThreadBlocks)`.
*/
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)hostShadowView
{
NSMutableSet *viewsWithNewFrames = [NSMutableSet setWithCapacity:1];
// This is nuanced. In the JS thread, we create a new update buffer
// `frameTags`/`frames` that is created/mutated in the JS thread. We access
// these structures in the UI-thread block. `NSMutableArray` is not thread
// safe so we rely on the fact that we never mutate it after it's passed to
// the main thread. To help protect against mutation, we alias the variable to
// a threadsafe `NSArray`, however the `NSArray` doesn't guarantee deep
// immutability so we must be very careful.
// https://developer.apple.com/library/mac/documentation/Cocoa/
// Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html
[hostShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:CGSizeMake(CSS_UNDEFINED, CSS_UNDEFINED)];
// Parallel arrays
NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
for (RCTShadowView *shadowView in viewsWithNewFrames) {
[frameReactTags addObject:shadowView.reactTag];
[frames addObject:[NSValue valueWithCGRect:shadowView.frame]];
[areNew addObject:@(shadowView.isNewView)];
[parentsAreNew addObject:@(shadowView.superview.isNewView)];
}
for (RCTShadowView *shadowView in viewsWithNewFrames) {
// We have to do this after we build the parentsAreNew array.
shadowView.newView = NO;
}
NSArray *immutableFrameReactTags = frameReactTags;
NSArray *immutableFrames = frames;
NSNumber *rootViewTag = hostShadowView.reactTag;
return ^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
for (NSUInteger ii = 0; ii < immutableFrames.count; ii++) {
NSNumber *reactTag = immutableFrameReactTags[ii];
UIView *view = viewRegistry[reactTag];
CGRect frame = [immutableFrames[ii] CGRectValue];
// These frames are in terms of anchorPoint = topLeft, but internally the
// views are anchorPoint = center for easier scale and rotation animations.
// Convert the frame so it works with anchorPoint = center.
__weak RCTUIManager *weakSelf = self;
[self setViewLayout:view
withAnchorPoint:CGPointMake(0.5, 0.5)
position:CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame))
bounds:CGRectMake(0, 0, frame.size.width, frame.size.height)
config:/*!isNew ? _layoutAnimationConfigMT.updateConfig : */nil // TODO: !!!
completion:^(BOOL finished) {
__strong RCTUIManager *strongSelf = weakSelf;
if (strongSelf->_layoutAnimationCallbackMT) {
strongSelf->_layoutAnimationCallbackMT(@[@(finished)]);
}
}];
}
RCTRootView *rootView = _viewRegistry[rootViewTag];
RCTTraverseViewNodes(rootView, ^(id<RCTViewNodeProtocol> view) {
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
[view reactBridgeDidFinishTransaction];
}
});
};
}
- (void)_amendPendingUIBlocksWithStylePropagationUpdateForRootView:(RCTShadowView *)topView
{
NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1];
[topView collectUpdatedProperties:applierBlocks parentProperties:@{}];
NSMutableArray *propsAppliers = [NSMutableArray arrayWithCapacity:applierBlocks.count];
for (RCTApplierBlock propsApplier in applierBlocks) {
[propsAppliers addObject:propsApplier];
}
NSArray *immutablePropsAppliers = propsAppliers;
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
for (NSUInteger f = 0; f < immutablePropsAppliers.count; f++) {
RCTApplierBlock applier = [immutablePropsAppliers objectAtIndex: f];
applier(viewRegistry);
}
}];
}
/**
* A method to be called from JS, which takes a container ID and then releases
* all subviews for that container upon receipt.
*/
- (void)removeSubviewsFromContainerWithID:(NSNumber *)containerID
{
RCT_EXPORT();
id<RCTViewNodeProtocol> container = _viewRegistry[containerID];
RCTAssert(container != nil, @"container view (for ID %@) not found", containerID);
NSUInteger subviewsCount = [[container reactSubviews] count];
NSMutableArray *indices = [[NSMutableArray alloc] initWithCapacity:subviewsCount];
for (NSInteger 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 *)_childrenToRemoveFromContainer:(id<RCTViewNodeProtocol>)container
atIndices:(NSArray *)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 *removedChildren = [NSMutableArray arrayWithCapacity:atIndices.count];
RCTCAssert(container != nil, @"container view (for ID %@) not found", container);
for (NSInteger i = 0; i < [atIndices count]; i++) {
NSInteger index = [atIndices[i] integerValue];
if (index < [[container reactSubviews] count]) {
[removedChildren addObject:[container reactSubviews][index]];
}
}
if (removedChildren.count != atIndices.count) {
RCTLogMustFix(@"removedChildren count (%tu) was not what we expected (%tu)", removedChildren.count, atIndices.count);
}
return removedChildren;
}
- (void)_removeChildren:(NSArray *)children fromContainer:(id<RCTViewNodeProtocol>)container
{
for (id removedChild in children) {
[container removeReactSubview:removedChild];
}
}
- (void)removeRootView:(NSNumber *)rootReactTag
{
RCT_EXPORT();
RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag];
RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag);
[self _purgeChildren:@[rootShadowView] fromRegistry:_shadowViewRegistry];
[_rootViewTags removeObject:rootReactTag];
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread();
UIView *rootView = viewRegistry[rootReactTag];
[viewManager _purgeChildren:@[rootView] fromRegistry:viewRegistry];
}];
}
- (void)replaceExistingNonRootView:(NSNumber *)reactTag withView:(NSNumber *)newReactTag
{
RCT_EXPORT();
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTAssert(shadowView != nil, @"shadowView (for ID %@) not found", reactTag);
RCTShadowView *superShadowView = shadowView.superview;
RCTAssert(superShadowView != nil, @"shadowView super (of ID %@) not found", reactTag);
NSUInteger indexOfView = [superShadowView.reactSubviews indexOfObject:shadowView];
RCTAssert(indexOfView != NSNotFound, @"View's superview doesn't claim it as subview (id %@)", reactTag);
NSArray *removeAtIndices = @[@(indexOfView)];
NSArray *addTags = @[newReactTag];
[self manageChildren:superShadowView.reactTag
moveFromIndices:nil
moveToIndices:nil
addChildReactTags:addTags
addAtIndices:removeAtIndices
removeAtIndices:removeAtIndices];
}
- (void)manageChildren:(NSNumber *)containerReactTag
moveFromIndices:(NSArray *)moveFromIndices
moveToIndices:(NSArray *)moveToIndices
addChildReactTags:(NSArray *)addChildReactTags
addAtIndices:(NSArray *)addAtIndices
removeAtIndices:(NSArray *)removeAtIndices
{
RCT_EXPORT();
[self _manageChildren:containerReactTag
moveFromIndices:moveFromIndices
moveToIndices:moveToIndices
addChildReactTags:addChildReactTags
addAtIndices:addAtIndices
removeAtIndices:removeAtIndices
registry:_shadowViewRegistry];
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread();
[viewManager _manageChildren:containerReactTag
moveFromIndices:moveFromIndices
moveToIndices:moveToIndices
addChildReactTags:addChildReactTags
addAtIndices:addAtIndices
removeAtIndices:removeAtIndices
registry:viewRegistry];
}];
}
- (void)_manageChildren:(NSNumber *)containerReactTag
moveFromIndices:(NSArray *)moveFromIndices
moveToIndices:(NSArray *)moveToIndices
addChildReactTags:(NSArray *)addChildReactTags
addAtIndices:(NSArray *)addAtIndices
removeAtIndices:(NSArray *)removeAtIndices
registry:(RCTSparseArray *)registry
{
id<RCTViewNodeProtocol> container = registry[containerReactTag];
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 *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
NSArray *temporarilyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
[self _removeChildren:permanentlyRemovedChildren fromContainer:container];
[self _removeChildren:temporarilyRemovedChildren fromContainer:container];
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
// TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient
// Figure out what to insert - merge temporary inserts and adds
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
for (NSInteger index = 0; index < temporarilyRemovedChildren.count; index++) {
destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index];
}
for (NSInteger index = 0; index < addAtIndices.count; index++) {
id view = registry[addChildReactTags[index]];
if (view) {
destinationsToChildrenToAdd[addAtIndices[index]] = view;
}
}
NSArray *sortedIndices = [[destinationsToChildrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)];
for (NSNumber *reactIndex in sortedIndices) {
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:[reactIndex integerValue]];
}
}
- (UIView *)viewForViewManager:(id <RCTNativeViewModule>)manager
{
return [manager viewWithEventDispatcher:_eventDispatcher];
}
static BOOL RCTCallPropertySetter(SEL setter, id value, id view, id defaultView, id <RCTNativeViewModule>manager)
{
// TODO: cache respondsToSelector tests
if ([manager respondsToSelector:setter]) {
if (value == [NSNull null]) {
value = nil;
}
((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView);
return YES;
}
return NO;
}
static void RCTSetViewProps(NSDictionary *props, UIView *view,
UIView *defaultView, id <RCTNativeViewModule>manager)
{
[props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forView:withDefaultView:", key]);
// For regular views we don't attempt to set properties
// unless the view property has been explicitly exported.
RCTCallPropertySetter(setter, obj, view, defaultView, manager);
}];
}
static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView,
RCTShadowView *defaultView, id <RCTNativeViewModule>manager)
{
[props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forShadowView:withDefaultView:", key]);
// For shadow views we call any custom setter methods by default,
// but if none is specified, we attempt to set property anyway.
if (!RCTCallPropertySetter(setter, obj, shadowView, defaultView, manager)) {
if (obj == [NSNull null]) {
// Copy property from default view to current
RCTSetProperty(shadowView, key, [defaultView valueForKey:key]);
} else {
RCTSetProperty(shadowView, key, obj);
}
}
}];
// Update layout
[shadowView updateShadowViewLayout];
}
- (void)createAndRegisterViewWithReactTag:(NSNumber *)reactTag
moduleName:(NSString *)moduleName
props:(NSDictionary *)props
{
RCT_EXPORT(createView);
id <RCTNativeViewModule>manager = [self _managerInstanceForViewWithModuleName:moduleName];
// Generate default view, used for resetting default props
if (!_defaultShadowViews[moduleName]) {
_defaultShadowViews[moduleName] = ([manager respondsToSelector:@selector(shadowView)] ? [manager shadowView] : nil) ?: [[RCTShadowView alloc] init];
}
RCTShadowView *shadowView = ([manager respondsToSelector:@selector(shadowView)] ? [manager shadowView] : nil) ?: [[RCTShadowView alloc] init];
shadowView.moduleName = moduleName;
shadowView.reactTag = reactTag;
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager);
_shadowViewRegistry[shadowView.reactTag] = shadowView;
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
RCTCAssertMainThread();
// Generate default view, used for resetting default props
if (!uiManager->_defaultViews[moduleName]) {
// Note the default is setup after the props are read for the first time ever
// for this className - this is ok because we only use the default for restoring
// defaults, which never happens on first creation.
uiManager->_defaultViews[moduleName] = [uiManager viewForViewManager:manager];
}
UIView *view = [uiManager viewForViewManager:manager];
if (view) {
// Set required properties
view.reactTag = reactTag;
view.multipleTouchEnabled = YES;
view.userInteractionEnabled = YES; // required for touch handling
view.layer.allowsGroupOpacity = YES; // required for touch handling
// Set custom properties
RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], manager);
}
viewRegistry[view.reactTag] = view;
}];
}
- (void)updateView:(NSNumber *)reactTag moduleName:(NSString *)moduleName props:(NSDictionary *)props
{
RCT_EXPORT();
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
id <RCTNativeViewModule>manager = [self _managerInstanceForViewWithModuleName:moduleName];
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[moduleName], manager);
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
RCTCAssertMainThread();
UIView *view = viewRegistry[reactTag];
RCTSetViewProps(props, view, uiManager->_defaultViews[moduleName], manager);
}];
}
- (void)becomeResponder:(NSNumber *)reactTag
{
RCT_EXPORT(focus);
if (!reactTag) return;
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
UIView *newResponder = viewRegistry[reactTag];
[newResponder becomeFirstResponder];
}];
}
- (void)resignResponder:(NSNumber *)reactTag
{
RCT_EXPORT(blur);
if (!reactTag) return;
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
UIView *currentResponder = viewRegistry[reactTag];
[currentResponder resignFirstResponder];
}];
}
- (void)batchDidComplete
{
// 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.
for (id <RCTNativeViewModule>viewManager in _viewManagers.allValues) {
RCTViewManagerUIBlock uiBlock = [viewManager respondsToSelector:@selector(uiBlockToAmendWithShadowViewRegistry:)] ? [viewManager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry] : nil;
if (uiBlock != nil) {
[self addUIBlock:uiBlock];
}
}
for (NSNumber *reactTag in _rootViewTags) {
RCTShadowView *rootView = _shadowViewRegistry[reactTag];
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
[self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootView];
}
pthread_mutex_lock(&_pendingUIBlocksMutex);
NSArray *previousPendingUIBlocks = _pendingUIBlocks;
_pendingUIBlocks = [[NSMutableArray alloc] init];
pthread_mutex_unlock(&_pendingUIBlocksMutex);
dispatch_async(dispatch_get_main_queue(), ^{
for (dispatch_block_t block in previousPendingUIBlocks) {
block();
}
});
}
- (void)layoutRootShadowView:(RCTShadowView *)rootShadowView
{
RCTViewManagerUIBlock uiBlock = [self uiBlockWithLayoutUpdateForRootView:rootShadowView];
__weak RCTUIManager *weakViewManager = self;
__weak RCTSparseArray *weakViewRegistry = _viewRegistry;
dispatch_async(dispatch_get_main_queue(), ^{
uiBlock(weakViewManager, weakViewRegistry);
});
}
- (void)measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
if (!callback) {
RCTLogError(@"Called measure with no callback");
return;
}
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view) {
RCTLogError(@"measure cannot find view with tag %zd", reactTag);
return;
}
CGRect frame = view.frame;
UIView *rootView = view;
while (rootView && ![rootView isReactRootView]) {
rootView = rootView.superview;
}
RCTCAssert([rootView isReactRootView], @"React view not inside RCTRootView");
// By convention, all coordinates, whether they be touch coordinates, or
// measurement coordinates are with respect to the root view.
CGPoint pagePoint = [view.superview convertPoint:frame.origin toView:rootView];
callback(@[
@(frame.origin.x),
@(frame.origin.y),
@(frame.size.width),
@(frame.size.height),
@(pagePoint.x),
@(pagePoint.y)
]);
}];
}
- (void)requestSchedulingJavaScriptNavigation:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
if (!callback || !errorCallback) {
RCTLogError(@"Callback not provided for navigation scheduling.");
return;
}
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
if (reactTag) {
//TODO: This is nasty - why is RCTNavigator hard-coded?
id rkObject = viewRegistry[reactTag];
if ([rkObject isKindOfClass:[RCTNavigator class]]) {
RCTNavigator *navigator = (RCTNavigator *)rkObject;
BOOL wasAcquired = [navigator requestSchedulingJavaScriptNavigation];
callback(@[@(wasAcquired)]);
} else {
NSString *msg =
[NSString stringWithFormat: @"Cannot set lock: Tag %@ is not an RCTNavigator", reactTag];
errorCallback(@[RCTAPIErrorObject(msg)]);
}
} else {
NSString *msg = [NSString stringWithFormat: @"Tag not specified for requestSchedulingJavaScriptNavigation"];
errorCallback(@[RCTAPIErrorObject(msg)]);
}
}];
}
/**
* TODO: This could be modified to accept any `RCTViewNodeProtocol`, if
* appropriate changes were made to that protocol to support `superview`
* traversal - which is possibly more difficult than it sounds since a
* `superview` is not a "react superview".
*/
+ (void)measureLayoutOnNodes:(RCTShadowView *)view
ancestor:(RCTShadowView *)ancestor
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback
{
if (!view) {
NSString *msg = [NSString stringWithFormat: @"Attempting to measure view that does not exist %@", view];
errorCallback(@[RCTAPIErrorObject(msg)]);
return;
}
if (!ancestor) {
NSString *msg = [NSString stringWithFormat: @"Attempting to measure relative to ancestor that does not exist %@", ancestor];
errorCallback(@[RCTAPIErrorObject(msg)]);
return;
}
CGRect result = [RCTShadowView measureLayout:view relativeTo:ancestor];
if (CGRectIsNull(result)) {
NSString *msg = [NSString stringWithFormat: @"view %@ is not an decendant of %@", view, ancestor];
errorCallback(@[RCTAPIErrorObject(msg)]);
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)) {
errorCallback(@[RCTAPIErrorObject(@"Attempted to measure layout but offset or dimensions were NaN")]);
return;
}
callback(@[@(topOffset), @(leftOffset), @(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).
*/
- (void)measureLayout:(NSNumber *)reactTag
relativeTo:(NSNumber *)ancestorReactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag];
[RCTUIManager measureLayoutOnNodes:shadowView ancestor:ancestorShadowView errorCallback:errorCallback callback: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).
*/
- (void)measureLayoutRelativeToParent:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
[RCTUIManager measureLayoutOnNodes:shadowView ancestor:[shadowView superview] errorCallback:errorCallback callback: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.
*/
- (void)measureViewsInRect:(NSDictionary *)rect
parentView:(NSNumber *)reactTag
errorCallback:(RCTResponseSenderBlock)errorCallback
callback:(RCTResponseSenderBlock)callback
{
RCT_EXPORT();
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
if (!shadowView) {
NSString *msg = [NSString stringWithFormat: @"Attempting to measure view that does not exist %@", shadowView];
errorCallback(@[RCTAPIErrorObject(msg)]);
return;
}
NSArray *childShadowViews = [shadowView reactSubviews];
NSMutableArray *results = [[NSMutableArray alloc] initWithCapacity:[childShadowViews count]];
CGRect layoutRect = [RCTConvert CGRect:rect];
for (int ii = 0; ii < [childShadowViews count]; ii++) {
RCTShadowView *childShadowView = [childShadowViews objectAtIndex:ii];
CGRect childLayout = [RCTShadowView measureLayout:childShadowView relativeTo:shadowView];
if (CGRectIsNull(childLayout)) {
NSString *msg = [NSString stringWithFormat: @"view %@ is not a decendant of %@", childShadowView, shadowView];
errorCallback(@[RCTAPIErrorObject(msg)]);
return;
}
CGFloat leftOffset = childLayout.origin.x;
CGFloat topOffset = childLayout.origin.y;
CGFloat width = childLayout.size.width;
CGFloat height = childLayout.size.height;
if (leftOffset <= layoutRect.origin.x + layoutRect.size.width &&
leftOffset + width >= layoutRect.origin.x &&
topOffset <= layoutRect.origin.y + layoutRect.size.height &&
topOffset + height >= layoutRect.origin.y) {
// This view is within the layout rect
NSDictionary *result = @{@"index": @(ii),
@"left": @(leftOffset),
@"top": @(topOffset),
@"width": @(width),
@"height": @(height)};
[results addObject:result];
}
}
callback(@[results]);
}
- (void)setMainScrollViewTag:(NSNumber *)reactTag
{
RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
// - There should be at most one designated "main scroll view"
// - There should be at most one designated "`nativeMainScrollDelegate`"
// - The one designated main scroll view should have the one designated
// `nativeMainScrollDelegate` set as its `nativeMainScrollDelegate`.
if (viewManager.mainScrollView) {
viewManager.mainScrollView.nativeMainScrollDelegate = nil;
}
if (reactTag) {
id rkObject = viewRegistry[reactTag];
if ([rkObject conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
viewManager.mainScrollView = (id<RCTScrollableProtocol>)rkObject;
((id<RCTScrollableProtocol>)rkObject).nativeMainScrollDelegate = viewManager.nativeMainScrollDelegate;
} else {
RCTCAssert(NO, @"Tag %@ does not conform to RCTScrollableProtocol", reactTag);
}
} else {
viewManager.mainScrollView = nil;
}
}];
}
- (void)scrollToOffsetWithView:(NSNumber *)reactTag scrollToOffsetX:(NSNumber *)offsetX offsetY:(NSNumber *)offsetY
{
RCT_EXPORT(scrollTo);
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id<RCTScrollableProtocol>)view scrollToOffset:CGPointMake([offsetX floatValue], [offsetY floatValue])];
} else {
RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag);
}
}];
}
- (void)zoomToRectWithView:(NSNumber *)reactTag rect:(NSDictionary *)rectDict
{
RCT_EXPORT(zoomToRect);
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry){
UIView *view = viewRegistry[reactTag];
if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) {
[(id<RCTScrollableProtocol>)view zoomToRect:[RCTConvert CGRect:rectDict] animated:YES];
} else {
RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag %@", view, reactTag);
}
}];
}
- (void)getScrollViewContentSize:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback failCallback:(RCTResponseSenderBlock)failCallback
{
RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag];
if (!view) {
NSString *error = [[NSString alloc] initWithFormat:@"cannot find view with tag %@", reactTag];
RCTLogError(@"%@", error);
failCallback(@[@{@"error": error}]);
return;
}
CGSize size = ((id<RCTScrollableProtocol>)view).contentSize;
NSDictionary *dict = @{@"width" : @(size.width), @"height" : @(size.height)};
callback(@[dict]);
}];
}
/**
* JS sets what *it* considers to be the responder. Later, scroll views can use
* this in order to determine if scrolling is appropriate.
*/
- (void)setJSResponder:(NSNumber *)reactTag
{
RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
_jsResponder = viewRegistry[reactTag];
if (!_jsResponder) {
RCTLogMustFix(@"Invalid view set to be the JS responder - tag %zd", reactTag);
}
}];
}
- (void)clearJSResponder
{
RCT_EXPORT();
[self addUIBlock:^(RCTUIManager *viewManager, RCTSparseArray *viewRegistry) {
_jsResponder = nil;
}];
}
- (NSDictionary *)allBubblingEventTypesConfigs
{
NSMutableDictionary *customBubblingEventTypesConfigs = [@{
// Bubble dispatched events
@"topTap": @{
@"phasedRegistrationNames": @{
@"bubbled": @"notActuallyTapDontUseMe",
@"captured": @"notActuallyTapCaptureDontUseMe"
}
},
@"topVisibleCellsChange": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onVisibleCellsChange",
@"captured": @"onVisibleCellsChangeCapture"
}
},
@"topNavigateBack": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onNavigationComplete",
@"captured": @"onNavigationCompleteCapture"
}
},
@"topNavRightButtonTap": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onNavRightButtonTap",
@"captured": @"onNavRightButtonTapCapture"
}
},
@"topChange": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onChange",
@"captured": @"onChangeCapture"
}
},
@"topFocus": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onFocus",
@"captured": @"onFocusCapture"
}
},
@"topBlur": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onBlur",
@"captured": @"onBlurCapture"
}
},
@"topSubmitEditing": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onSubmitEditing",
@"captured": @"onSubmitEditingCapture"
}
},
@"topEndEditing": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onEndEditing",
@"captured": @"onEndEditingCapture"
}
},
@"topTextInput": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onTextInput",
@"captured": @"onTextInputCapture"
}
},
@"topTouchStart": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onTouchStart",
@"captured": @"onTouchStartCapture"
}
},
@"topTouchMove": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onTouchMove",
@"captured": @"onTouchMoveCapture"
}
},
@"topTouchCancel": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onTouchCancel",
@"captured": @"onTouchCancelCapture"
}
},
@"topTouchEnd": @{
@"phasedRegistrationNames": @{
@"bubbled": @"onTouchEnd",
@"captured": @"onTouchEndCapture"
}
},
} mutableCopy];
for (id <RCTNativeViewModule> viewManager in _viewManagers.allValues) {
NSDictionary *bubblingEvents = [viewManager respondsToSelector:@selector(customBubblingEventTypes)]? [viewManager customBubblingEventTypes] : nil;
if (bubblingEvents) {
for (NSString *eventName in bubblingEvents) {
RCTCAssert(!customBubblingEventTypesConfigs[eventName], @"Event %@ registered multiple times.", eventName);
}
[customBubblingEventTypesConfigs addEntriesFromDictionary:bubblingEvents];
}
}
return customBubblingEventTypesConfigs;
}
- (NSDictionary *)allDirectEventTypesConfigs
{
NSMutableDictionary *customDirectEventTypes = [@{
@"topScrollBeginDrag": @{
@"registrationName": @"onScrollBeginDrag"
},
@"topScroll": @{
@"registrationName": @"onScroll"
},
@"topScrollEndDrag": @{
@"registrationName": @"onScrollEndDrag"
},
@"topScrollAnimationEnd": @{
@"registrationName": @"onScrollAnimationEnd"
},
@"topSelectionChange": @{
@"registrationName": @"onSelectionChange"
},
@"topMomentumScrollBegin": @{
@"registrationName": @"onMomentumScrollBegin"
},
@"topMomentumScrollEnd": @{
@"registrationName": @"onMomentumScrollEnd"
},
@"topPullToRefresh": @{
@"registrationName": @"onPullToRefresh"
},
@"topLoadingStart": @{
@"registrationName": @"onLoadingStart"
},
@"topLoadingFinish": @{
@"registrationName": @"onLoadingFinish"
},
@"topLoadingError": @{
@"registrationName": @"onLoadingError"
},
} mutableCopy];
for (id <RCTNativeViewModule> viewManager in _viewManagers.allValues) {
NSDictionary *bubblingEvents = [viewManager respondsToSelector:@selector(customDirectEventTypes)] ? [viewManager customDirectEventTypes] : nil;
if (bubblingEvents) {
for (NSString *eventName in bubblingEvents) {
RCTCAssert(!customDirectEventTypes[eventName], @"Event %@ registered multiple times.", eventName);
}
[customDirectEventTypes addEntriesFromDictionary:bubblingEvents];
}
}
return customDirectEventTypes;
}
- (NSDictionary *)constantsToExport
{
UIScreen *screen = [UIScreen mainScreen];
NSMutableDictionary *allJSConstants = [@{
@"customBubblingEventTypes": [self allBubblingEventTypesConfigs],
@"customDirectEventTypes": [self allDirectEventTypesConfigs],
@"NSTextAlignment": @{
@"Left": @(NSTextAlignmentLeft),
@"Center": @(NSTextAlignmentCenter),
@"Right": @(NSTextAlignmentRight),
},
@"Dimensions": @{
@"window": @{
@"width": @(screen.bounds.size.width),
@"height": @(screen.bounds.size.height),
@"scale": @(screen.scale),
},
@"modalFullscreenView": @{
@"width": @(screen.bounds.size.width),
@"height": @(screen.bounds.size.height),
},
},
@"StyleConstants": @{
@"PointerEventsValues": @{
@"none": @(RCTPointerEventsNone),
@"boxNone": @(RCTPointerEventsBoxNone),
@"boxOnly": @(RCTPointerEventsBoxOnly),
@"unspecified": @(RCTPointerEventsUnspecified),
},
},
@"UIText": @{
@"AutocapitalizationType": @{
@"AllCharacters": @(UITextAutocapitalizationTypeAllCharacters),
@"Sentences": @(UITextAutocapitalizationTypeSentences),
@"Words": @(UITextAutocapitalizationTypeWords),
@"None": @(UITextAutocapitalizationTypeNone),
},
},
@"UIView": @{
@"ContentMode": @{
@"ScaleToFill": @(UIViewContentModeScaleToFill),
@"ScaleAspectFit": @(UIViewContentModeScaleAspectFit),
@"ScaleAspectFill": @(UIViewContentModeScaleAspectFill),
@"Redraw": @(UIViewContentModeRedraw),
@"Center": @(UIViewContentModeCenter),
@"Top": @(UIViewContentModeTop),
@"Bottom": @(UIViewContentModeBottom),
@"Left": @(UIViewContentModeLeft),
@"Right": @(UIViewContentModeRight),
@"TopLeft": @(UIViewContentModeTopLeft),
@"TopRight": @(UIViewContentModeTopRight),
@"BottomLeft": @(UIViewContentModeBottomLeft),
@"BottomRight": @(UIViewContentModeBottomRight),
},
},
} mutableCopy];
[_viewManagers enumerateKeysAndObjectsUsingBlock:^(NSString *name, id <RCTNativeViewModule> viewManager, BOOL *stop) {
NSDictionary *constants = [viewManager respondsToSelector:@selector(constantsToExport)] ? [viewManager constantsToExport] : nil;
if (constants) {
RCTAssert(allJSConstants[name] == nil , @"Cannot redefine constant namespace: %@", name);
// add an additional 'Constants' namespace for each class
allJSConstants[name] = @{@"Constants": constants};
}
}];
return allJSConstants;
}
- (void)configureNextLayoutAnimation:(NSDictionary *)config withCallback:(RCTResponseSenderBlock)callback errorCallback:(RCTResponseSenderBlock)errorCallback
{
RCT_EXPORT();
if (_nextLayoutAnimationCallback || _nextLayoutAnimationConfig) {
RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimationConfig, config);
}
if (config[@"delete"] != nil) {
RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config);
}
_nextLayoutAnimationConfig = config;
_nextLayoutAnimationCallback = callback;
}
static UIView *_jsResponder;
+ (UIView *)JSResponder
{
return _jsResponder;
}
@end