/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTUIManager.h" #import #import #import "Layout.h" #import "RCTAnimationType.h" #import "RCTAssert.h" #import "RCTBridge.h" #import "RCTConvert.h" #import "RCTDefines.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTProfile.h" #import "RCTRootView.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTUtils.h" #import "RCTView.h" #import "RCTViewManager.h" #import "RCTViewNodeProtocol.h" #import "UIView+React.h" static void RCTTraverseViewNodes(id view, void (^block)(id)); static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps); @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @property (nonatomic, readonly) NSTimeInterval delay; @property (nonatomic, readonly, copy) NSString *property; @property (nonatomic, readonly) id fromValue; @property (nonatomic, readonly) id toValue; @property (nonatomic, readonly) CGFloat springDamping; @property (nonatomic, readonly) CGFloat initialVelocity; @property (nonatomic, readonly) RCTAnimationType animationType; @end @implementation RCTAnimation static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnimationType type) { switch (type) { case RCTAnimationTypeLinear: return UIViewAnimationOptionCurveLinear; case RCTAnimationTypeEaseIn: return UIViewAnimationOptionCurveEaseIn; case RCTAnimationTypeEaseOut: return UIViewAnimationOptionCurveEaseOut; case RCTAnimationTypeEaseInEaseOut: return UIViewAnimationOptionCurveEaseInOut; case RCTAnimationTypeKeyboard: // http://stackoverflow.com/questions/18870447/how-to-use-the-default-ios7-uianimation-curve return (UIViewAnimationOptions)(7 << 16); default: RCTLogError(@"Unsupported animation type %zd", type); return UIViewAnimationOptionCurveEaseInOut; } } - (instancetype)initWithDuration:(NSTimeInterval)duration dictionary:(NSDictionary *)config { if (!config) { return nil; } if ((self = [super init])) { _property = [RCTConvert NSString:config[@"property"]]; _duration = [RCTConvert NSTimeInterval:config[@"duration"]] ?: duration; if (_duration > 0.0 && _duration < 0.01) { RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds."); _duration = _duration * 1000.0; } _delay = [RCTConvert NSTimeInterval:config[@"delay"]]; if (_delay > 0.0 && _delay < 0.01) { RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds."); _delay = _delay * 1000.0; } _animationType = [RCTConvert RCTAnimationType:config[@"type"]]; if (_animationType == RCTAnimationTypeSpring) { _springDamping = [RCTConvert CGFloat:config[@"springDamping"]]; _initialVelocity = [RCTConvert CGFloat:config[@"initialVelocity"]]; } _fromValue = config[@"fromValue"]; _toValue = config[@"toValue"]; } return self; } - (void)performAnimations:(void (^)(void))animations withCompletionBlock:(void (^)(BOOL completed))completionBlock { if (_animationType == RCTAnimationTypeSpring) { [UIView animateWithDuration:_duration delay:_delay usingSpringWithDamping:_springDamping initialSpringVelocity:_initialVelocity options:UIViewAnimationOptionBeginFromCurrentState animations:animations completion:completionBlock]; } else { UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionsFromRCTAnimationType(_animationType); [UIView animateWithDuration:_duration delay:_delay options:options animations:animations completion:completionBlock]; } } @end @interface RCTLayoutAnimation : NSObject @property (nonatomic, copy) NSDictionary *config; @property (nonatomic, strong) RCTAnimation *createAnimation; @property (nonatomic, strong) RCTAnimation *updateAnimation; @property (nonatomic, strong) RCTAnimation *deleteAnimation; @property (nonatomic, copy) RCTResponseSenderBlock callback; @end @implementation RCTLayoutAnimation - (instancetype)initWithDictionary:(NSDictionary *)config callback:(RCTResponseSenderBlock)callback { if (!config) { return nil; } if ((self = [super init])) { _config = [config copy]; NSTimeInterval duration = [RCTConvert NSTimeInterval:config[@"duration"]]; if (duration > 0.0 && duration < 0.01) { RCTLogError(@"RCTLayoutAnimation expects timings to be in ms, not seconds."); duration = duration * 1000.0; } _createAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"create"]]; _updateAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"update"]]; _deleteAnimation = [[RCTAnimation alloc] initWithDuration:duration dictionary:config[@"delete"]]; _callback = callback; } return self; } @end @interface RCTUIManager () // NOTE: these are properties so that they can be accessed by unit tests @property (nonatomic, strong) RCTSparseArray *viewManagerRegistry; // RCT thread only @property (nonatomic, strong) RCTSparseArray *shadowViewRegistry; // RCT thread only @property (nonatomic, strong) RCTSparseArray *viewRegistry; // Main thread only @end @implementation RCTUIManager { dispatch_queue_t _shadowQueue; // Root views are only mutated on the shadow queue NSMutableSet *_rootViewTags; NSMutableArray *_pendingUIBlocks; NSLock *_pendingUIBlocksLock; // Animation RCTLayoutAnimation *_nextLayoutAnimation; // RCT thread only RCTLayoutAnimation *_layoutAnimation; // Main thread only // Keyed by viewName NSMutableDictionary *_defaultShadowViews; // RCT thread only NSMutableDictionary *_defaultViews; // Main thread only NSDictionary *_viewManagers; NSDictionary *_viewConfigs; NSMutableSet *_bridgeTransactionListeners; } @synthesize bridge = _bridge; RCT_EXPORT_MODULE() /** * Declared in RCTBridge. */ extern NSString *RCTBridgeModuleNameForClass(Class cls); /** * This function derives the view name automatically * from the module name. */ static NSString *RCTViewNameForModuleName(NSString *moduleName) { NSString *name = moduleName; RCTAssert(name.length, @"Invalid moduleName '%@'", moduleName); if ([name hasSuffix:@"Manager"]) { name = [name substringToIndex:name.length - @"Manager".length]; } return name; } // TODO: only send name once instead of a dictionary of name and type keyed by name static NSDictionary *RCTViewConfigForModule(Class managerClass) { unsigned int count = 0; Method *methods = class_copyMethodList(object_getClass(managerClass), &count); NSMutableDictionary *props = [[NSMutableDictionary alloc] initWithCapacity:count]; for (unsigned int i = 0; i < count; i++) { Method method = methods[i]; NSString *methodName = NSStringFromSelector(method_getName(method)); if ([methodName hasPrefix:@"getPropConfig"]) { NSRange nameRange = [methodName rangeOfString:@"_"]; if (nameRange.length) { NSString *name = [methodName substringFromIndex:nameRange.location + 1]; NSString *type = [managerClass valueForKey:methodName]; props[name] = type; } } } free(methods); return props; } - (instancetype)init { if ((self = [super init])) { _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); _pendingUIBlocksLock = [[NSLock alloc] init]; _defaultShadowViews = [[NSMutableDictionary alloc] init]; _defaultViews = [[NSMutableDictionary alloc] init]; _viewManagerRegistry = [[RCTSparseArray alloc] init]; _shadowViewRegistry = [[RCTSparseArray alloc] init]; _viewRegistry = [[RCTSparseArray alloc] init]; // Internal resources _pendingUIBlocks = [[NSMutableArray alloc] init]; _rootViewTags = [[NSMutableSet alloc] init]; _bridgeTransactionListeners = [[NSMutableSet alloc] init]; } return self; } - (BOOL)isValid { return _viewRegistry != nil; } - (void)invalidate { /** * Called on the JS Thread since all modules are invalidated on the JS thread */ dispatch_async(dispatch_get_main_queue(), ^{ for (NSNumber *rootViewTag in _rootViewTags) { [_viewRegistry[rootViewTag] invalidate]; } _rootViewTags = nil; _shadowViewRegistry = nil; _viewRegistry = nil; _bridgeTransactionListeners = nil; _bridge = nil; [_pendingUIBlocksLock lock]; _pendingUIBlocks = nil; [_pendingUIBlocksLock unlock]; }); } - (void)setBridge:(RCTBridge *)bridge { RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance"); _bridge = bridge; _shadowViewRegistry = [[RCTSparseArray alloc] init]; // Get view managers from bridge NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init]; NSMutableDictionary *viewConfigs = [[NSMutableDictionary alloc] init]; [_bridge.modules enumerateKeysAndObjectsUsingBlock: ^(NSString *moduleName, RCTViewManager *manager, __unused BOOL *stop) { if ([manager isKindOfClass:[RCTViewManager class]]) { NSString *viewName = RCTViewNameForModuleName(moduleName); viewManagers[viewName] = manager; viewConfigs[viewName] = RCTViewConfigForModule([manager class]); } }]; _viewManagers = [viewManagers copy]; _viewConfigs = [viewConfigs copy]; } - (dispatch_queue_t)methodQueue { return _shadowQueue; } - (void)registerRootView:(UIView *)rootView { RCTAssertMainThread(); 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); // Register view _viewRegistry[reactTag] = rootView; CGRect frame = rootView.frame; // Register shadow view __weak RCTUIManager *weakSelf = self; dispatch_async(_shadowQueue, ^{ RCTUIManager *strongSelf = weakSelf; if (!strongSelf.isValid) { return; } RCTShadowView *shadowView = [[RCTShadowView alloc] init]; shadowView.reactTag = reactTag; shadowView.frame = frame; shadowView.backgroundColor = rootView.backgroundColor; shadowView.viewName = NSStringFromClass([rootView class]); strongSelf->_shadowViewRegistry[shadowView.reactTag] = shadowView; [strongSelf->_rootViewTags addObject:reactTag]; }); } - (UIView *)viewForReactTag:(NSNumber *)reactTag { RCTAssertMainThread(); return _viewRegistry[reactTag]; } - (void)setFrame:(CGRect)frame forRootView:(UIView *)rootView { RCTAssertMainThread(); NSNumber *reactTag = rootView.reactTag; RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); dispatch_async(_shadowQueue, ^{ RCTShadowView *rootShadowView = _shadowViewRegistry[reactTag]; RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); rootShadowView.frame = frame; [rootShadowView updateLayout]; [self batchDidComplete]; }); } - (void)setBackgroundColor:(UIColor *)color forRootView:(UIView *)rootView { RCTAssertMainThread(); NSNumber *reactTag = rootView.reactTag; RCTAssert(RCTIsReactRootView(reactTag), @"Specified view %@ is not a root view", reactTag); __weak RCTUIManager *weakSelf = self; dispatch_async(_shadowQueue, ^{ RCTUIManager *strongSelf = weakSelf; if (!strongSelf.isValid) { return; } RCTShadowView *rootShadowView = strongSelf->_shadowViewRegistry[reactTag]; RCTAssert(rootShadowView != nil, @"Could not locate root view with tag #%@", reactTag); rootShadowView.backgroundColor = color; [self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootShadowView]; [self flushUIBlocks]; }); } /** * Unregisters views from registries */ - (void)_purgeChildren:(NSArray *)children fromRegistry:(RCTSparseArray *)registry { for (id child in children) { RCTTraverseViewNodes(registry[child.reactTag], ^(id subview) { RCTAssert(![subview isReactRootView], @"Root views should not be unregistered"); if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) { [(id)subview invalidate]; } registry[subview.reactTag] = nil; if (registry == _viewRegistry) { [_bridgeTransactionListeners removeObject:subview]; } }); } } - (void)addUIBlock:(RCTViewManagerUIBlock)block { RCTAssertThread(_shadowQueue, @"-[RCTUIManager addUIBlock:] should only be called from the " "UIManager's _shadowQueue (it may be accessed via `bridge.uiManager.methodQueue`)"); if (!block) { return; } if (!self.isValid) { return; } __weak RCTUIManager *weakViewManager = self; dispatch_block_t outerBlock = ^{ RCTUIManager *strongViewManager = weakViewManager; if (strongViewManager && strongViewManager.isValid) { block(strongViewManager, strongViewManager->_viewRegistry); } }; [_pendingUIBlocksLock lock]; [_pendingUIBlocks addObject:outerBlock]; [_pendingUIBlocksLock unlock]; } - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTShadowView *)rootShadowView { RCTAssert(![NSThread isMainThread], @"Should be called on shadow thread"); 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. [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; NSSet *originalViewsWithNewFrames = [viewsWithNewFrames copy]; NSMutableArray *viewsToCheck = [viewsWithNewFrames.allObjects mutableCopy]; while (viewsToCheck.count > 0) { // Better to remove from the front and append to the end // because of how NSMutableArray is implemented. // (It's a "round" buffer with stored size and offset.) RCTShadowView *viewToCheck = viewsToCheck.firstObject; [viewsToCheck removeObjectAtIndex:0]; if (viewToCheck.layoutOnly) { [viewsWithNewFrames removeObject:viewToCheck]; [viewsToCheck addObjectsFromArray:[viewToCheck reactSubviews]]; } else { [viewsWithNewFrames addObject:viewToCheck]; } } // Parallel arrays are built and then handed off to main thread NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *areNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *parentsAreNew = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { CGRect frame = shadowView.adjustedFrame; NSNumber *reactTag = shadowView.reactTag; [frameReactTags addObject:reactTag]; [frames addObject:[NSValue valueWithCGRect:frame]]; [areNew addObject:@(shadowView.isNewView)]; RCTShadowView *superview = shadowView; BOOL parentIsNew = NO; while (YES) { superview = superview.superview; parentIsNew = superview.isNewView; if (!superview.layoutOnly) { break; } } [parentsAreNew addObject:@(parentIsNew)]; id event = shadowView.hasOnLayout ? RCTShadowViewOnLayoutEventPayload(shadowView.reactTag, frame) : (id)kCFNull; [onLayoutEvents addObject:event]; } for (RCTShadowView *shadowView in originalViewsWithNewFrames) { // We have to do this after we build the parentsAreNew array. shadowView.newView = NO; } // These are blocks to be executed on each view, immediately after // reactSetFrame: has been called. Note that if reactSetFrame: is not called, // these won't be called either, so this is not a suitable place to update // properties that aren't related to layout. NSMutableArray *updateBlocks = [[NSMutableArray alloc] init]; for (RCTShadowView *shadowView in viewsWithNewFrames) { RCTViewManager *manager = _viewManagerRegistry[shadowView.reactTag]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; if (block) [updateBlocks addObject:block]; } // Perform layout (possibly animated) return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTResponseSenderBlock callback = uiManager->_layoutAnimation.callback; __block NSUInteger completionsCalled = 0; for (NSUInteger ii = 0; ii < frames.count; ii++) { NSNumber *reactTag = frameReactTags[ii]; UIView *view = viewRegistry[reactTag]; if (!view) { continue; } CGRect frame = [frames[ii] CGRectValue]; id event = onLayoutEvents[ii]; BOOL isNew = [areNew[ii] boolValue]; RCTAnimation *updateAnimation = isNew ? nil : uiManager->_layoutAnimation.updateAnimation; BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; RCTAnimation *createAnimation = shouldAnimateCreation ? uiManager->_layoutAnimation.createAnimation : nil; void (^completion)(BOOL) = ^(BOOL finished) { completionsCalled++; if (event != (id)kCFNull) { [uiManager.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event]; } if (callback && completionsCalled == frames.count - 1) { callback(@[@(finished)]); } }; // Animate view update if (updateAnimation) { [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; for (RCTViewManagerUIBlock block in updateBlocks) { block(uiManager, viewRegistry); } } withCompletionBlock:completion]; } else { [view reactSetFrame:frame]; for (RCTViewManagerUIBlock block in updateBlocks) { block(uiManager, viewRegistry); } completion(YES); } // Animate view creation if (createAnimation) { if ([createAnimation.property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); } else if ([createAnimation.property isEqualToString:@"opacity"]) { view.layer.opacity = 0.0; } [createAnimation performAnimations:^{ if ([createAnimation.property isEqual:@"scaleXY"]) { view.layer.transform = CATransform3DIdentity; } else if ([createAnimation.property isEqual:@"opacity"]) { view.layer.opacity = 1.0; } else { RCTLogError(@"Unsupported layout animation createConfig property %@", createAnimation.property); } for (RCTViewManagerUIBlock block in updateBlocks) { block(uiManager, viewRegistry); } } withCompletionBlock:nil]; } } /** * TODO(tadeu): Remove it once and for all */ for (id node in _bridgeTransactionListeners) { [node reactBridgeDidFinishTransaction]; } }; } - (void)_amendPendingUIBlocksWithStylePropagationUpdateForRootView:(RCTShadowView *)topView { NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { for (RCTApplierBlock block in applierBlocks) { block(viewRegistry); } }]; } /** * 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:(NSNumber *)containerID) { id container = _shadowViewRegistry[containerID]; RCTAssert(container != nil, @"container view (for ID %@) not found", containerID); NSUInteger subviewsCount = [container reactSubviews].count; NSMutableArray *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 *)_childrenToRemoveFromContainer:(id)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]; 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) { RCTLogMustFix(@"removedChildren count (%tu) was not what we expected (%tu)", removedChildren.count, atIndices.count); } return removedChildren; } - (void)_removeChildren:(NSArray *)children fromContainer:(id)container { for (id removedChild in children) { [container removeReactSubview:removedChild]; } } RCT_EXPORT_METHOD(removeRootView:(NSNumber *)rootReactTag) { RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag]; RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag); [self _purgeChildren:rootShadowView.reactSubviews fromRegistry:_shadowViewRegistry]; _shadowViewRegistry[rootReactTag] = nil; [_rootViewTags removeObject:rootReactTag]; [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ RCTAssertMainThread(); UIView *rootView = viewRegistry[rootReactTag]; [uiManager _purgeChildren:rootView.reactSubviews fromRegistry:viewRegistry]; viewRegistry[rootReactTag] = nil; }]; } RCT_EXPORT_METHOD(replaceExistingNonRootView:(NSNumber *)reactTag withView:(NSNumber *)newReactTag) { 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]; } /** * This method modifies the indices received in manageChildren() to take into * account views that are layout only. For example, if JS tells native to insert * view with tag 12 at index 4, but view 12 is layout only, we would want to * insert its children's tags, tags 13 and 14, at indices 4 and 5 instead. This * causes us to have to shift the remaining indices to account for the new * views. */ - (void)modifyManageChildren:(NSNumber *)containerReactTag addChildReactTags:(NSMutableArray *)mutableAddChildReactTags addAtIndices:(NSMutableArray *)mutableAddAtIndices removeAtIndices:(NSMutableArray *)mutableRemoveAtIndices { NSUInteger i; NSMutableArray *containerSubviews = [[_shadowViewRegistry[containerReactTag] reactSubviews] mutableCopy]; i = 0; while (i < containerSubviews.count) { RCTShadowView *shadowView = containerSubviews[i]; if (!shadowView.layoutOnly) { i++; continue; } [containerSubviews removeObjectAtIndex:i]; NSArray *subviews = [shadowView reactSubviews]; NSUInteger subviewsCount = subviews.count; NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)]; [containerSubviews insertObjects:subviews atIndexes:insertionIndexes]; NSUInteger removalIndex = [mutableRemoveAtIndices indexOfObject:@(i)]; if (removalIndex != NSNotFound) { [mutableRemoveAtIndices removeObjectAtIndex:removalIndex]; } if (subviewsCount != 1) { for (NSUInteger j = 0, count = mutableRemoveAtIndices.count; j < count; j++) { NSUInteger atIndex = [mutableRemoveAtIndices[j] unsignedIntegerValue]; if (atIndex > i) { mutableRemoveAtIndices[j] = @(atIndex + subviewsCount - 1); } } } if (removalIndex != NSNotFound) { for (NSUInteger j = 0; j < subviewsCount; j++) { [mutableRemoveAtIndices insertObject:@(i + j) atIndex:removalIndex + j]; } } if (removalIndex == NSNotFound && subviewsCount != 1) { for (NSUInteger j = 0, count = mutableAddAtIndices.count; j < count; j++) { NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue]; if (atIndex > i) { mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1); } } } } i = 0; while (i < mutableAddChildReactTags.count) { NSNumber *tag = mutableAddChildReactTags[i]; NSNumber *index = mutableAddAtIndices[i]; RCTShadowView *shadowView = _shadowViewRegistry[tag]; if (!shadowView.layoutOnly) { i++; continue; } NSArray *subviews = [shadowView reactSubviews]; NSUInteger subviewsCount = subviews.count; [mutableAddAtIndices removeObjectAtIndex:i]; [mutableAddChildReactTags removeObjectAtIndex:i]; for (NSUInteger j = 0; j < subviewsCount; j++) { [mutableAddChildReactTags insertObject:[subviews[j] reactTag] atIndex:i + j]; [mutableAddAtIndices insertObject:@(index.unsignedIntegerValue + j) atIndex:i + j]; } for (NSUInteger j = i + subviewsCount, count = mutableAddAtIndices.count; j < count; j++) { NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue]; mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1); } } } - (NSNumber *)containerReactTag:(NSNumber *)containerReactTag offset:(inout NSUInteger *)offset { RCTShadowView *container = _shadowViewRegistry[containerReactTag]; NSNumber *containerSuperviewReactTag = containerReactTag; RCTShadowView *superview = container; while (superview.layoutOnly) { RCTShadowView *superviewSuperview = superview.superview; containerSuperviewReactTag = superviewSuperview.reactTag; NSMutableArray *reactSubviews = [[superviewSuperview reactSubviews] mutableCopy]; NSUInteger superviewIndex = [reactSubviews indexOfObject:superview]; NSUInteger i = 0; while (i < superviewIndex) { RCTShadowView *child = reactSubviews[i]; if (!child.layoutOnly) { if (offset) { (*offset)++; } i++; continue; } [reactSubviews removeObjectAtIndex:i]; NSArray *subviews = [child reactSubviews]; NSUInteger subviewsCount = subviews.count; NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)]; [reactSubviews insertObjects:subviews atIndexes:insertionIndexes]; superviewIndex += subviewsCount - 1; } superview = superviewSuperview; } return containerSuperviewReactTag; } RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices) { RCTShadowView *container = _shadowViewRegistry[containerReactTag]; NSUInteger offset = 0; NSNumber *containerSuperviewReactTag = [self containerReactTag:containerReactTag offset:&offset]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"Invalid argument: moveFromIndices.count != moveToIndices.count"); if (moveFromIndices.count > 0) { NSMutableArray *mutableAddChildReactTags = [addChildReactTags mutableCopy]; NSMutableArray *mutableAddAtIndices = [addAtIndices mutableCopy]; NSMutableArray *mutableRemoveAtIndices = [removeAtIndices mutableCopy]; NSArray *containerSubviews = [container reactSubviews]; for (NSUInteger i = 0, count = moveFromIndices.count; i < count; i++) { NSNumber *from = moveFromIndices[i]; NSNumber *to = moveToIndices[i]; [mutableAddChildReactTags addObject:[containerSubviews[from.unsignedIntegerValue] reactTag]]; [mutableAddAtIndices addObject:to]; [mutableRemoveAtIndices addObject:from]; } addChildReactTags = mutableAddChildReactTags; addAtIndices = mutableAddAtIndices; removeAtIndices = mutableRemoveAtIndices; } NSMutableArray *mutableAddChildReactTags; NSMutableArray *mutableAddAtIndices; NSMutableArray *mutableRemoveAtIndices; if (containerSuperviewReactTag) { mutableAddChildReactTags = [addChildReactTags mutableCopy]; mutableAddAtIndices = [addAtIndices mutableCopy]; mutableRemoveAtIndices = [removeAtIndices mutableCopy]; [self modifyManageChildren:containerReactTag addChildReactTags:mutableAddChildReactTags addAtIndices:mutableAddAtIndices removeAtIndices:mutableRemoveAtIndices]; if (offset > 0) { NSUInteger count = MAX(mutableAddAtIndices.count, mutableRemoveAtIndices.count); for (NSUInteger i = 0; i < count; i++) { if (i < mutableAddAtIndices.count) { NSUInteger index = [mutableAddAtIndices[i] unsignedIntegerValue]; mutableAddAtIndices[i] = @(index + offset); } if (i < mutableRemoveAtIndices.count) { NSUInteger index = [mutableRemoveAtIndices[i] unsignedIntegerValue]; mutableRemoveAtIndices[i] = @(index + offset); } } } } [self _manageChildren:containerReactTag addChildReactTags:addChildReactTags addAtIndices:addAtIndices removeAtIndices:removeAtIndices registry:_shadowViewRegistry]; if (containerSuperviewReactTag) { [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ (void)(id []){containerReactTag, @(offset), addChildReactTags, addAtIndices, removeAtIndices}; [uiManager _manageChildren:containerSuperviewReactTag addChildReactTags:mutableAddChildReactTags addAtIndices:mutableAddAtIndices removeAtIndices:mutableRemoveAtIndices registry:viewRegistry]; }]; } } - (void)_manageChildren:(NSNumber *)containerReactTag addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices registry:(RCTSparseArray *)registry { id container = registry[containerReactTag]; RCTAssert(addChildReactTags.count == addAtIndices.count, @"Invalid arguments: addChildReactTags.count == addAtIndices.count"); // Removes are using "before" indices NSArray *removedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; [self _removeChildren:removedChildren fromContainer:container]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT reactTag in %@", addChildReactTags]; NSArray *permanentlyRemovedChildren = [removedChildren filteredArrayUsingPredicate:predicate]; [self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry]; // TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient // Figure out what to insert NSMutableDictionary *childrenToAdd = [NSMutableDictionary dictionary]; for (NSInteger index = 0, count = addAtIndices.count; index < count; index++) { id view = registry[addChildReactTags[index]]; if (view) { childrenToAdd[addAtIndices[index]] = view; } } NSArray *sortedIndices = [[childrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)]; for (NSNumber *reactIndex in sortedIndices) { [container insertReactSubview:childrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } } static BOOL RCTCallPropertySetter(NSString *key, SEL setter, id value, id view, id defaultView, RCTViewManager *manager) { // TODO: cache respondsToSelector tests if ([manager respondsToSelector:setter]) { if (value == (id)kCFNull) { value = nil; } void (^block)() = ^{ ((void (*)(id, SEL, id, id, id))objc_msgSend)(manager, setter, value, view, defaultView); }; if (RCT_DEBUG) { NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([manager class])); NSString *logPrefix = [NSString stringWithFormat: @"Error setting property '%@' of %@ with tag #%@: ", key, viewName, [view reactTag]]; RCTPerformBlockWithLogPrefix(block, logPrefix); } else { block(); } return YES; } return NO; } static void RCTSetViewProps(NSDictionary *props, UIView *view, UIView *defaultView, RCTViewManager *manager) { [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, __unused BOOL *stop) { SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forView:withDefaultView:", key]); RCTCallPropertySetter(key, setter, obj, view, defaultView, manager); }]; } static void RCTSetShadowViewProps(NSDictionary *props, RCTShadowView *shadowView, RCTShadowView *defaultView, RCTViewManager *manager) { [props enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, __unused BOOL *stop) { SEL setter = NSSelectorFromString([NSString stringWithFormat:@"set_%@:forShadowView:withDefaultView:", key]); RCTCallPropertySetter(key, setter, obj, shadowView, defaultView, manager); }]; // Update layout [shadowView updateLayout]; } RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag viewName:(NSString *)viewName rootTag:(__unused NSNumber *)rootTag props:(NSDictionary *)props) { RCTViewManager *manager = _viewManagers[viewName]; if (manager == nil) { RCTLogWarn(@"No manager class found for view with module name \"%@\"", viewName); manager = [[RCTViewManager alloc] init]; } // Register manager _viewManagerRegistry[reactTag] = manager; RCTShadowView *shadowView = [manager shadowView]; if (shadowView) { // Generate default view, used for resetting default props if (!_defaultShadowViews[viewName]) { _defaultShadowViews[viewName] = [manager shadowView]; } // Set properties shadowView.viewName = viewName; shadowView.reactTag = reactTag; shadowView.allProps = props; RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager); } _shadowViewRegistry[reactTag] = shadowView; if (!shadowView.layoutOnly) { // Shadow view is the source of truth for background color this is a little // bit counter-intuitive if people try to set background color when setting up // the view, but it's the only way that makes sense given our threading model UIColor *backgroundColor = shadowView.backgroundColor; [self addUIBlock:^(RCTUIManager *uiManager, __unused RCTSparseArray *viewRegistry) { [uiManager createView:reactTag viewName:viewName props:props withManager:manager backgroundColor:backgroundColor]; }]; } } - (UIView *)createView:(NSNumber *)reactTag viewName:(NSString *)viewName props:(NSDictionary *)props withManager:(RCTViewManager *)manager backgroundColor:(UIColor *)backgroundColor { RCTAssertMainThread(); UIView *view = [manager view]; if (!view) { return nil; } // Generate default view, used for resetting default props if (!_defaultViews[viewName]) { // 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. _defaultViews[viewName] = [manager view]; } // Set properties view.reactTag = reactTag; view.backgroundColor = backgroundColor; if ([view isKindOfClass:[UIView class]]) { view.multipleTouchEnabled = YES; view.userInteractionEnabled = YES; // required for touch handling view.layer.allowsGroupOpacity = YES; // required for touch handling } RCTSetViewProps(props, view, _defaultViews[viewName], manager); if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { [_bridgeTransactionListeners addObject:view]; } _viewRegistry[reactTag] = view; return view; } NS_INLINE BOOL RCTRectIsDefined(CGRect frame) { return !(isnan(frame.origin.x) || isnan(frame.origin.y) || isnan(frame.size.width) || isnan(frame.size.height)); } NS_INLINE NSDictionary *RCTShadowViewOnLayoutEventPayload(NSNumber *reactTag, CGRect frame) { return @{ @"target": reactTag, @"layout": @{ @"x": @(frame.origin.x), @"y": @(frame.origin.y), @"width": @(frame.size.width), @"height": @(frame.size.height), }, }; } // TODO: remove viewName param as it isn't needed RCT_EXPORT_METHOD(updateView:(NSNumber *)reactTag viewName:(__unused NSString *)_ props:(NSDictionary *)props) { RCTViewManager *viewManager = _viewManagerRegistry[reactTag]; NSString *viewName = RCTViewNameForModuleName(RCTBridgeModuleNameForClass([viewManager class])); RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], viewManager); const BOOL wasLayoutOnly = shadowView.layoutOnly; NSDictionary *newProps = RCTPropsMerge(shadowView.allProps, props); shadowView.allProps = newProps; const BOOL isLayoutOnly = shadowView.layoutOnly; if (wasLayoutOnly != isLayoutOnly) { // Add/remove node if (isLayoutOnly) { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTAssertMainThread(); UIView *container = viewRegistry[reactTag]; const CGRect containerFrame = container.frame; const CGFloat deltaX = containerFrame.origin.x; const CGFloat deltaY = containerFrame.origin.y; NSUInteger offset = [container.superview.subviews indexOfObject:container]; [container.subviews enumerateObjectsUsingBlock:^(UIView *subview, NSUInteger idx, __unused BOOL *stop) { [container removeReactSubview:subview]; CGRect subviewFrame = subview.frame; subviewFrame.origin.x += deltaX; subviewFrame.origin.y += deltaY; subview.frame = subviewFrame; [container.superview insertReactSubview:subview atIndex:idx + offset]; }]; [container.superview removeReactSubview:container]; if ([container conformsToProtocol:@protocol(RCTInvalidating)]) { [(id)container invalidate]; } viewRegistry[reactTag] = nil; }]; } else { NSMutableArray *mutableAddChildReactTags = [[[shadowView reactSubviews] valueForKey:@"reactTag"] mutableCopy]; NSMutableArray *mutableAddAtIndices = [NSMutableArray arrayWithCapacity:mutableAddChildReactTags.count]; for (NSUInteger i = 0, count = mutableAddChildReactTags.count; i < count; i++) { [mutableAddAtIndices addObject:@(i)]; } [self modifyManageChildren:reactTag addChildReactTags:mutableAddChildReactTags addAtIndices:mutableAddAtIndices removeAtIndices:nil]; NSUInteger offset = [shadowView.superview.reactSubviews indexOfObject:shadowView]; NSNumber *containerSuperviewReactTag = [self containerReactTag:shadowView.superview.reactTag offset:&offset]; UIColor *backgroundColor = shadowView.backgroundColor; CGRect shadowViewFrame = shadowView.adjustedFrame; NSMutableDictionary *newFrames = [NSMutableDictionary dictionaryWithCapacity:mutableAddChildReactTags.count]; for (NSNumber *childTag in mutableAddChildReactTags) { RCTShadowView *child = _shadowViewRegistry[childTag]; newFrames[childTag] = [NSValue valueWithCGRect:child.adjustedFrame]; } [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { RCTAssertMainThread(); UIView *containerSuperview = viewRegistry[containerSuperviewReactTag]; UIView *container = [uiManager createView:reactTag viewName:viewName props:newProps withManager:viewManager backgroundColor:backgroundColor]; [containerSuperview insertReactSubview:container atIndex:offset]; if (RCTRectIsDefined(shadowViewFrame)) { container.frame = shadowViewFrame; } for (NSUInteger i = 0, count = mutableAddAtIndices.count; i < count; i++) { NSNumber *tag = mutableAddChildReactTags[i]; UIView *subview = viewRegistry[tag]; [containerSuperview removeReactSubview:subview]; NSUInteger atIndex = [mutableAddAtIndices[i] unsignedIntegerValue]; [container insertReactSubview:subview atIndex:atIndex]; CGRect subviewFrame = [newFrames[tag] CGRectValue]; if (RCTRectIsDefined(subviewFrame)) { subview.frame = subviewFrame; } } }]; } } else if (!isLayoutOnly) { // Update node [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager); }]; } } RCT_EXPORT_METHOD(focus:(NSNumber *)reactTag) { if (!reactTag) return; [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *newResponder = viewRegistry[reactTag]; [newResponder reactWillMakeFirstResponder]; [newResponder becomeFirstResponder]; [newResponder reactDidMakeFirstResponder]; }]; } RCT_EXPORT_METHOD(blur:(NSNumber *)reactTag) { if (!reactTag) return; [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *currentResponder = viewRegistry[reactTag]; [currentResponder resignFirstResponder]; }]; } RCT_EXPORT_METHOD(findSubviewIn:(NSNumber *)reactTag atPoint:(CGPoint)point callback:(RCTResponseSenderBlock)callback) { if (!reactTag) { callback(@[(id)kCFNull]); return; } [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *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), ]); }]; } - (void)batchDidComplete { RCTProfileBeginEvent(); // Gather blocks to be executed now that all view hierarchy manipulations have // been completed (note that these may still take place before layout has finished) for (RCTViewManager *manager in _viewManagers.allValues) { RCTViewManagerUIBlock uiBlock = [manager uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; [self addUIBlock:uiBlock]; } // Set up next layout animation if (_nextLayoutAnimation) { RCTLayoutAnimation *layoutAnimation = _nextLayoutAnimation; [self addUIBlock:^(RCTUIManager *uiManager, __unused RCTSparseArray *viewRegistry) { uiManager->_layoutAnimation = layoutAnimation; }]; } // Perform layout for (NSNumber *reactTag in _rootViewTags) { RCTShadowView *rootView = _shadowViewRegistry[reactTag]; [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; [self _amendPendingUIBlocksWithStylePropagationUpdateForRootView:rootView]; } // Clear layout animations if (_nextLayoutAnimation) { [self addUIBlock:^(RCTUIManager *uiManager, __unused RCTSparseArray *viewRegistry) { uiManager->_layoutAnimation = nil; }]; _nextLayoutAnimation = nil; } RCTProfileEndEvent(@"[RCTUIManager batchDidComplete]", @"uimanager", @{ @"view_count": @([_viewRegistry count]), }); [self flushUIBlocks]; } - (void)flushUIBlocks { // 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. [_pendingUIBlocksLock lock]; NSArray *previousPendingUIBlocks = _pendingUIBlocks; _pendingUIBlocks = [[NSMutableArray alloc] init]; [_pendingUIBlocksLock unlock]; // Execute the previously queued UI blocks RCTProfileBeginFlowEvent(); dispatch_async(dispatch_get_main_queue(), ^{ RCTProfileEndFlowEvent(); RCTProfileBeginEvent(); for (dispatch_block_t block in previousPendingUIBlocks) { block(); } RCTProfileEndEvent(@"UIManager flushUIBlocks", @"objc_call", @{ @"count": @(previousPendingUIBlocks.count), }); }); } RCT_EXPORT_METHOD(measure:(NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { if (!callback) { RCTLogError(@"Called measure with no callback"); return; } [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { // this view was probably collapsed out RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); callback(@[]); return; } CGRect frame = view.frame; UIView *rootView = view; while (rootView && ![rootView isReactRootView]) { rootView = rootView.superview; } // TODO: this doesn't work because sometimes view is inside a modal window // RCTAssert([rootView isReactRootView], @"React view is not inside a React root view"); // 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) ]); }]; } 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 decendant 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:(NSNumber *)reactTag relativeTo:(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:(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:(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 *childShadowViews = [shadowView reactSubviews]; NSMutableArray *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 decendant 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(setMainScrollViewTag:(NSNumber *)reactTag) { [self addUIBlock:^(RCTUIManager *uiManager, 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 (uiManager.mainScrollView) { uiManager.mainScrollView.nativeMainScrollDelegate = nil; } if (reactTag) { id view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { uiManager.mainScrollView = (id)view; uiManager.mainScrollView.nativeMainScrollDelegate = uiManager.nativeMainScrollDelegate; } else { RCTAssert(NO, @"Tag #%@ does not conform to RCTScrollableProtocol", reactTag); } } else { uiManager.mainScrollView = nil; } }]; } // TODO: we could just pass point property RCT_EXPORT_METHOD(scrollTo:(NSNumber *)reactTag withOffsetX:(CGFloat)offsetX offsetY:(CGFloat)offsetY) { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:(CGPoint){offsetX, offsetY} animated:YES]; } else { RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } // TODO: we could just pass point property RCT_EXPORT_METHOD(scrollWithoutAnimationTo:(NSNumber *)reactTag offsetX:(CGFloat)offsetX offsetY:(CGFloat)offsetY) { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view scrollToOffset:(CGPoint){offsetX, offsetY} animated:NO]; } else { RCTLogError(@"tried to scrollToOffset: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag withRect:(CGRect)rect) { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ UIView *view = viewRegistry[reactTag]; if ([view conformsToProtocol:@protocol(RCTScrollableProtocol)]) { [(id)view zoomToRect:rect animated:YES]; } else { RCTLogError(@"tried to zoomToRect: on non-RCTScrollableProtocol view %@ with tag #%@", view, reactTag); } }]; } /** * 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:(NSNumber *)reactTag blockNativeResponder:(__unused BOOL)blockNativeResponder) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; if (!shadowView) { RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag); } else if (shadowView.layoutOnly) { RCTLogError(@"Cannot set JS responder to layout-only view with tag %@ - %@, %@", reactTag, shadowView, shadowView.allProps); } else { [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = viewRegistry[reactTag]; }]; } } RCT_EXPORT_METHOD(clearJSResponder) { [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused RCTSparseArray *viewRegistry) { _jsResponder = nil; }]; } // TODO: these event types should be distributed among the modules // that declare them. Also, events should be registerable by any class // that can call event handlers, not just UIViewManagers. This code // also seems highly redundant - every event has the same properties. - (NSDictionary *)customBubblingEventTypes { NSMutableDictionary *customBubblingEventTypesConfigs = [@{ // Bubble dispatched events @"topTap": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onPress", @"captured": @"onPressCapture" } }, @"topVisibleCellsChange": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onVisibleCellsChange", @"captured": @"onVisibleCellsChangeCapture" } }, @"topNavigateBack": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onNavigationComplete", @"captured": @"onNavigationCompleteCapture" } }, @"topNavLeftButtonTap": @{ @"phasedRegistrationNames": @{ @"bubbled": @"onNavLeftButtonTap", @"captured": @"onNavLefttButtonTapCapture" } }, @"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 (RCTViewManager *manager in _viewManagers.allValues) { if (RCTClassOverridesInstanceMethod([manager class], @selector(customBubblingEventTypes))) { NSDictionary *eventTypes = [manager customBubblingEventTypes]; for (NSString *eventName in eventTypes) { RCTAssert(!customBubblingEventTypesConfigs[eventName], @"Event '%@' registered multiple times.", eventName); } [customBubblingEventTypesConfigs addEntriesFromDictionary:eventTypes]; } }; return customBubblingEventTypesConfigs; } - (NSDictionary *)customDirectEventTypes { NSMutableDictionary *customDirectEventTypes = [@{ @"topScrollBeginDrag": @{ @"registrationName": @"onScrollBeginDrag" }, @"topScroll": @{ @"registrationName": @"onScroll" }, @"topScrollEndDrag": @{ @"registrationName": @"onScrollEndDrag" }, @"topScrollAnimationEnd": @{ @"registrationName": @"onScrollAnimationEnd" }, @"topLayout": @{ @"registrationName": @"onLayout" }, @"topSelectionChange": @{ @"registrationName": @"onSelectionChange" }, @"topMomentumScrollBegin": @{ @"registrationName": @"onMomentumScrollBegin" }, @"topMomentumScrollEnd": @{ @"registrationName": @"onMomentumScrollEnd" }, @"topPullToRefresh": @{ @"registrationName": @"onPullToRefresh" }, @"topLoadingStart": @{ @"registrationName": @"onLoadingStart" }, @"topLoadingFinish": @{ @"registrationName": @"onLoadingFinish" }, @"topLoadingError": @{ @"registrationName": @"onLoadingError" }, @"topAccessibilityTap": @{ @"registrationName": @"onAccessibilityTap" }, @"topMagicTap": @{ @"registrationName": @"onMagicTap" }, } mutableCopy]; for (RCTViewManager *manager in _viewManagers.allValues) { if (RCTClassOverridesInstanceMethod([manager class], @selector(customDirectEventTypes))) { NSDictionary *eventTypes = [manager customDirectEventTypes]; for (NSString *eventName in eventTypes) { RCTAssert(!customDirectEventTypes[eventName], @"Event '%@' registered multiple times.", eventName); } [customDirectEventTypes addEntriesFromDictionary:eventTypes]; } }; return customDirectEventTypes; } - (NSDictionary *)constantsToExport { NSMutableDictionary *allJSConstants = [@{ @"customBubblingEventTypes": [self customBubblingEventTypes], @"customDirectEventTypes": [self customDirectEventTypes], @"Dimensions": @{ @"window": @{ @"width": @(RCTScreenSize().width), @"height": @(RCTScreenSize().height), @"scale": @(RCTScreenScale()), }, @"modalFullscreenView": @{ @"width": @(RCTScreenSize().width), @"height": @(RCTScreenSize().height), }, }, } mutableCopy]; [_viewManagers enumerateKeysAndObjectsUsingBlock: ^(NSString *name, RCTViewManager *manager, __unused BOOL *stop) { NSMutableDictionary *constantsNamespace = [NSMutableDictionary dictionaryWithDictionary:allJSConstants[name]]; // Add custom constants // TODO: should these be inherited? NSDictionary *constants = RCTClassOverridesInstanceMethod([manager class], @selector(constantsToExport)) ? [manager constantsToExport] : nil; if (constants.count) { RCTAssert(constantsNamespace[@"Constants"] == nil , @"Cannot redefine Constants in namespace: %@", name); // add an additional 'Constants' namespace for each class constantsNamespace[@"Constants"] = constants; } // Add native props constantsNamespace[@"NativeProps"] = _viewConfigs[name]; allJSConstants[name] = [constantsNamespace copy]; }]; return allJSConstants; } RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config withCallback:(RCTResponseSenderBlock)callback errorCallback:(__unused RCTResponseSenderBlock)errorCallback) { if (_nextLayoutAnimation && ![config isEqualToDictionary:_nextLayoutAnimation.config]) { RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", _nextLayoutAnimation.config, config); } if (config[@"delete"] != nil) { RCTLogError(@"LayoutAnimation only supports create and update right now. Config: %@", config); } _nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config callback:callback]; } static UIView *_jsResponder; + (UIView *)JSResponder { return _jsResponder; } @end @implementation RCTBridge (RCTUIManager) - (RCTUIManager *)uiManager { return self.modules[RCTBridgeModuleNameForClass([RCTUIManager class])]; } @end static void RCTTraverseViewNodes(id view, void (^block)(id)) { if (view.reactTag) block(view); for (id subview in view.reactSubviews) { RCTTraverseViewNodes(subview, block); } } static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps) { NSMutableDictionary *afterProps = [NSMutableDictionary dictionaryWithDictionary:beforeProps]; // Can't use -addEntriesFromDictionary: because we want to remove keys with NSNull values. [newProps enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) { if (obj == (id)kCFNull) { [afterProps removeObjectForKey:key]; } else { afterProps[key] = obj; } }]; return afterProps; }