/** * 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 "RCTAccessibilityManager.h" #import "RCTAnimationType.h" #import "RCTAssert.h" #import "RCTBridge+Private.h" #import "RCTBridge.h" #import "RCTComponent.h" #import "RCTComponentData.h" #import "RCTConvert.h" #import "RCTDefines.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTModuleData.h" #import "RCTModuleMethod.h" #import "RCTProfile.h" #import "RCTRootContentView.h" #import "RCTRootShadowView.h" #import "RCTRootViewInternal.h" #import "RCTScrollableProtocol.h" #import "RCTShadowView.h" #import "RCTUIManagerObserverCoordinator.h" #import "RCTUtils.h" #import "RCTView.h" #import "RCTViewManager.h" #import "UIView+React.h" static void RCTTraverseViewNodes(id view, void (^block)(id)) { if (view.reactTag) { block(view); for (id subview in view.reactSubviews) { RCTTraverseViewNodes(subview, block); } } } char *const RCTUIManagerQueueName = "com.facebook.react.ShadowQueue"; NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification = @"RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification"; NSString *const RCTUIManagerDidRegisterRootViewNotification = @"RCTUIManagerDidRegisterRootViewNotification"; NSString *const RCTUIManagerDidRemoveRootViewNotification = @"RCTUIManagerDidRemoveRootViewNotification"; NSString *const RCTUIManagerRootViewKey = @"RCTUIManagerRootViewKey"; @interface RCTAnimation : NSObject @property (nonatomic, readonly) NSTimeInterval duration; @property (nonatomic, readonly) NSTimeInterval delay; @property (nonatomic, readonly, copy) NSString *property; @property (nonatomic, readonly) CGFloat springDamping; @property (nonatomic, readonly) CGFloat initialVelocity; @property (nonatomic, readonly) RCTAnimationType animationType; @end static UIViewAnimationCurve _currentKeyboardAnimationCurve; @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)(_currentKeyboardAnimationCurve << 16); default: RCTLogError(@"Unsupported animation type %zd", type); return UIViewAnimationOptionCurveEaseInOut; } } // Use a custom initialization function rather than implementing `+initialize` so that we can control // when the initialization code runs. `+initialize` runs immediately before the first message is sent // to the class which may be too late for us. By this time, we may have missed some // `UIKeyboardWillChangeFrameNotification`s. + (void)initializeStatics { #if !TARGET_OS_TV static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; }); #endif } + (void)keyboardWillChangeFrame:(NSNotification *)notification { #if !TARGET_OS_TV NSDictionary *userInfo = notification.userInfo; _currentKeyboardAnimationCurve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; #endif } - (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"]]; } } 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 @implementation RCTUIManager { // Root views are only mutated on the shadow queue NSMutableSet *_rootViewTags; NSMutableArray *_pendingUIBlocks; // Animation RCTLayoutAnimation *_layoutAnimation; // Main thread only NSMutableSet *_viewsToBeDeleted; // Main thread only NSMutableDictionary *_shadowViewRegistry; // RCT thread only NSMutableDictionary *_viewRegistry; // Main thread only // Keyed by viewName NSDictionary *_componentDataByName; NSMutableSet> *_bridgeTransactionListeners; } @synthesize bridge = _bridge; RCT_EXPORT_MODULE() - (void)didReceiveNewContentSizeMultiplier { // Report the event across the bridge. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateContentSizeMultiplier" body:@([_bridge.accessibilityManager multiplier])]; #pragma clang diagnostic pop dispatch_async(RCTGetUIManagerQueue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification object:self]; [self setNeedsLayout]; }); } - (void)invalidate { /** * Called on the JS Thread since all modules are invalidated on the JS thread */ // This only accessed from the shadow queue _pendingUIBlocks = nil; dispatch_async(dispatch_get_main_queue(), ^{ RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"UIManager invalidate", nil); for (NSNumber *rootViewTag in self->_rootViewTags) { [(id)self->_viewRegistry[rootViewTag] invalidate]; } self->_rootViewTags = nil; self->_shadowViewRegistry = nil; self->_viewRegistry = nil; self->_bridgeTransactionListeners = nil; self->_bridge = nil; [[NSNotificationCenter defaultCenter] removeObserver:self]; RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); }); } - (NSMutableDictionary *)shadowViewRegistry { // NOTE: this method only exists so that it can be accessed by unit tests if (!_shadowViewRegistry) { _shadowViewRegistry = [NSMutableDictionary new]; } return _shadowViewRegistry; } - (NSMutableDictionary *)viewRegistry { // NOTE: this method only exists so that it can be accessed by unit tests if (!_viewRegistry) { _viewRegistry = [NSMutableDictionary new]; } return _viewRegistry; } - (void)setBridge:(RCTBridge *)bridge { RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance"); _bridge = bridge; _shadowViewRegistry = [NSMutableDictionary new]; _viewRegistry = [NSMutableDictionary new]; // Internal resources _pendingUIBlocks = [NSMutableArray new]; _rootViewTags = [NSMutableSet new]; _bridgeTransactionListeners = [NSMutableSet new]; _observerCoordinator = [RCTUIManagerObserverCoordinator new]; _viewsToBeDeleted = [NSMutableSet new]; // Get view managers from bridge NSMutableDictionary *componentDataByName = [NSMutableDictionary new]; for (Class moduleClass in _bridge.moduleClasses) { if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) { RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass bridge:_bridge]; componentDataByName[componentData.name] = componentData; } } _componentDataByName = [componentDataByName copy]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveNewContentSizeMultiplier) name:RCTAccessibilityManagerDidUpdateMultiplierNotification object:_bridge.accessibilityManager]; [RCTAnimation initializeStatics]; } dispatch_queue_t RCTGetUIManagerQueue(void) { static dispatch_queue_t shadowQueue; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ if ([NSOperation instancesRespondToSelector:@selector(qualityOfService)]) { dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0); shadowQueue = dispatch_queue_create(RCTUIManagerQueueName, attr); } else { shadowQueue = dispatch_queue_create(RCTUIManagerQueueName, DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(shadowQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); } }); return shadowQueue; } BOOL RCTIsUIManagerQueue() { static void *queueKey = &queueKey; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dispatch_queue_set_specific(RCTGetUIManagerQueue(), queueKey, queueKey, NULL); }); return dispatch_get_specific(queueKey) == queueKey; } - (dispatch_queue_t)methodQueue { return RCTGetUIManagerQueue(); } - (void)registerRootView:(RCTRootContentView *)rootView { RCTAssertMainQueue(); NSNumber *reactTag = rootView.reactTag; RCTAssert(RCTIsReactRootView(reactTag), @"View %@ with tag #%@ is not a root view", rootView, reactTag); UIView *existingView = _viewRegistry[reactTag]; RCTAssert(existingView == nil || existingView == rootView, @"Expect all root views to have unique tag. Added %@ twice", reactTag); CGSize availableSize = rootView.availableSize; // Register view _viewRegistry[reactTag] = rootView; // Register shadow view dispatch_async(RCTGetUIManagerQueue(), ^{ if (!self->_viewRegistry) { return; } RCTRootShadowView *shadowView = [RCTRootShadowView new]; shadowView.availableSize = availableSize; shadowView.reactTag = reactTag; shadowView.backgroundColor = rootView.backgroundColor; shadowView.viewName = NSStringFromClass([rootView class]); self->_shadowViewRegistry[shadowView.reactTag] = shadowView; [self->_rootViewTags addObject:reactTag]; }); [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerDidRegisterRootViewNotification object:self userInfo:@{RCTUIManagerRootViewKey: rootView}]; } - (NSString *)viewNameForReactTag:(NSNumber *)reactTag { RCTAssertThread(RCTGetUIManagerQueue(), @"viewNameForReactTag can only be called from the shadow queue"); return _shadowViewRegistry[reactTag].viewName; } - (UIView *)viewForReactTag:(NSNumber *)reactTag { RCTAssertMainQueue(); return _viewRegistry[reactTag]; } - (RCTShadowView *)shadowViewForReactTag:(NSNumber *)reactTag { RCTAssertUIManagerQueue(); return _shadowViewRegistry[reactTag]; } - (void)setAvailableSize:(CGSize)availableSize forRootView:(UIView *)rootView { RCTAssertMainQueue(); NSNumber *reactTag = rootView.reactTag; dispatch_async(RCTGetUIManagerQueue(), ^{ RCTRootShadowView *shadowView = (RCTRootShadowView *)self->_shadowViewRegistry[reactTag]; RCTAssert(shadowView != nil, @"Could not locate shadow view with tag #%@", reactTag); RCTAssert([shadowView isKindOfClass:[RCTRootShadowView class]], @"Located shadow view (with tag #%@) is actually not root view.", reactTag); if (CGSizeEqualToSize(availableSize, shadowView.availableSize)) { return; } shadowView.availableSize = availableSize; [self setNeedsLayout]; }); } - (void)setSize:(CGSize)size forView:(UIView *)view { RCTAssertMainQueue(); NSNumber *reactTag = view.reactTag; dispatch_async(RCTGetUIManagerQueue(), ^{ RCTShadowView *shadowView = self->_shadowViewRegistry[reactTag]; RCTAssert(shadowView != nil, @"Could not locate shadow view with tag #%@", reactTag); if (CGSizeEqualToSize(size, shadowView.size)) { return; } shadowView.size = size; [self setNeedsLayout]; }); } - (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view { RCTAssertMainQueue(); NSNumber *reactTag = view.reactTag; dispatch_async(RCTGetUIManagerQueue(), ^{ RCTShadowView *shadowView = self->_shadowViewRegistry[reactTag]; RCTAssert(shadowView != nil, @"Could not locate view with tag #%@", reactTag); if (!CGSizeEqualToSize(shadowView.intrinsicContentSize, size)) { shadowView.intrinsicContentSize = size; [self setNeedsLayout]; } }); } - (void)setBackgroundColor:(UIColor *)color forView:(UIView *)view { RCTAssertMainQueue(); NSNumber *reactTag = view.reactTag; dispatch_async(RCTGetUIManagerQueue(), ^{ if (!self->_viewRegistry) { return; } RCTShadowView *shadowView = self->_shadowViewRegistry[reactTag]; RCTAssert(shadowView != nil, @"Could not locate root view with tag #%@", reactTag); shadowView.backgroundColor = color; [self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:shadowView]; [self flushUIBlocks]; }); } /** * Unregisters views from registries */ - (void)_purgeChildren:(NSArray> *)children fromRegistry:(NSMutableDictionary> *)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 removeObjectForKey:subview.reactTag]; if (registry == (NSMutableDictionary> *)self->_viewRegistry) { [self->_bridgeTransactionListeners removeObject:subview]; } }); } } - (void)addUIBlock:(RCTViewManagerUIBlock)block { RCTAssertThread(RCTGetUIManagerQueue(), @"-[RCTUIManager addUIBlock:] should only be called from the " "UIManager's queue (get this using `RCTGetUIManagerQueue()`)"); if (!block || !_viewRegistry) { return; } [_pendingUIBlocks addObject:block]; } - (void)prependUIBlock:(RCTViewManagerUIBlock)block { RCTAssertThread(RCTGetUIManagerQueue(), @"-[RCTUIManager prependUIBlock:] should only be called from the " "UIManager's queue (get this using `RCTGetUIManagerQueue()`)"); if (!block || !_viewRegistry) { return; } [_pendingUIBlocks insertObject:block atIndex:0]; } - (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView { RCTAssert(!RCTIsMainQueue(), @"Should be called on shadow queue"); // 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. NSSet *viewsWithNewFrames = [rootShadowView collectViewsWithUpdatedFrames]; if (!viewsWithNewFrames.count) { // no frame change results in no UI update block return nil; } typedef struct { CGRect frame; UIUserInterfaceLayoutDirection layoutDirection; BOOL isNew; BOOL parentIsNew; BOOL isHidden; } RCTFrameData; // Construct arrays then hand off to main thread NSUInteger count = viewsWithNewFrames.count; NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count]; NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count]; { NSUInteger index = 0; RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes; for (RCTShadowView *shadowView in viewsWithNewFrames) { reactTags[index] = shadowView.reactTag; frameDataArray[index++] = (RCTFrameData){ shadowView.frame, shadowView.effectiveLayoutDirection, shadowView.isNewView, shadowView.superview.isNewView, shadowView.isHidden, }; } } // 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. NSMutableDictionary *updateBlocks = [NSMutableDictionary new]; for (RCTShadowView *shadowView in viewsWithNewFrames) { // We have to do this after we build the parentsAreNew array. shadowView.newView = NO; NSNumber *reactTag = shadowView.reactTag; RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager]; RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView]; if (block) { updateBlocks[reactTag] = block; } if (shadowView.onLayout) { CGRect frame = shadowView.frame; shadowView.onLayout(@{ @"layout": @{ @"x": @(frame.origin.x), @"y": @(frame.origin.y), @"width": @(frame.size.width), @"height": @(frame.size.height), }, }); } if (RCTIsReactRootView(reactTag)) { CGSize contentSize = shadowView.frame.size; dispatch_async(dispatch_get_main_queue(), ^{ UIView *view = self->_viewRegistry[reactTag]; RCTAssert(view != nil, @"view (for ID %@) not found", reactTag); RCTRootView *rootView = (RCTRootView *)[view superview]; rootView.intrinsicContentSize = contentSize; }); } } // Perform layout (possibly animated) return ^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes; RCTLayoutAnimation *layoutAnimation = uiManager->_layoutAnimation; __block NSUInteger completionsCalled = 0; NSInteger index = 0; for (NSNumber *reactTag in reactTags) { RCTFrameData frameData = frameDataArray[index++]; UIView *view = viewRegistry[reactTag]; CGRect frame = frameData.frame; BOOL isHidden = frameData.isHidden; UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection; BOOL isNew = frameData.isNew; RCTAnimation *updateAnimation = isNew ? nil : layoutAnimation.updateAnimation; BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew; RCTAnimation *createAnimation = shouldAnimateCreation ? layoutAnimation.createAnimation : nil; void (^completion)(BOOL) = ^(BOOL finished) { completionsCalled++; if (layoutAnimation.callback && completionsCalled == count) { layoutAnimation.callback(@[@(finished)]); // It's unsafe to call this callback more than once, so we nil it out here // to make sure that doesn't happen. layoutAnimation.callback = nil; } }; if (view.isHidden != isHidden) { view.hidden = isHidden; } if (view.reactLayoutDirection != layoutDirection) { view.reactLayoutDirection = layoutDirection; } RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag]; if (createAnimation) { // Animate view creation [view reactSetFrame:frame]; CATransform3D finalTransform = view.layer.transform; CGFloat finalOpacity = view.layer.opacity; NSString *property = createAnimation.property; if ([property isEqualToString:@"scaleXY"]) { view.layer.transform = CATransform3DMakeScale(0, 0, 0); } else if ([property isEqualToString:@"opacity"]) { view.layer.opacity = 0.0; } else { RCTLogError(@"Unsupported layout animation createConfig property %@", createAnimation.property); } [createAnimation performAnimations:^{ if ([property isEqualToString:@"scaleXY"]) { view.layer.transform = finalTransform; } else if ([property isEqualToString:@"opacity"]) { view.layer.opacity = finalOpacity; } if (updateBlock) { updateBlock(self, viewRegistry); } } withCompletionBlock:completion]; } else if (updateAnimation) { // Animate view update [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; if (updateBlock) { updateBlock(self, viewRegistry); } } withCompletionBlock:completion]; } else { // Update without animation [view reactSetFrame:frame]; if (updateBlock) { updateBlock(self, viewRegistry); } completion(YES); } } // Clean up uiManager->_layoutAnimation = nil; }; } - (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowView *)topView { NSMutableSet *applierBlocks = [NSMutableSet setWithCapacity:1]; [topView collectUpdatedProperties:applierBlocks parentProperties:@{}]; if (applierBlocks.count) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *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:(nonnull 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) { NSString *message = [NSString stringWithFormat:@"removedChildren count (%tu) was not what we expected (%tu)", removedChildren.count, atIndices.count]; RCTFatal(RCTErrorWithMessage(message)); } return removedChildren; } - (void)_removeChildren:(NSArray> *)children fromContainer:(id)container { for (id removedChild in children) { [container removeReactSubview:removedChild]; } } /** * Remove subviews from their parent with an animation. */ - (void)_removeChildren:(NSArray *)children fromContainer:(UIView *)container withAnimation:(RCTLayoutAnimation *)animation { RCTAssertMainQueue(); RCTAnimation *deleteAnimation = animation.deleteAnimation; __block NSUInteger completionsCalled = 0; for (UIView *removedChild in children) { void (^completion)(BOOL) = ^(BOOL finished) { completionsCalled++; [self->_viewsToBeDeleted removeObject:removedChild]; [container removeReactSubview:removedChild]; if (animation.callback && completionsCalled == children.count) { animation.callback(@[@(finished)]); // It's unsafe to call this callback more than once, so we nil it out here // to make sure that doesn't happen. animation.callback = nil; } }; [_viewsToBeDeleted addObject:removedChild]; // Disable user interaction while the view is animating since JS won't receive // the view events anyway. removedChild.userInteractionEnabled = NO; NSString *property = deleteAnimation.property; [deleteAnimation performAnimations:^{ if ([property isEqualToString:@"scaleXY"]) { removedChild.layer.transform = CATransform3DMakeScale(0.001, 0.001, 0.001); } else if ([property isEqualToString:@"opacity"]) { removedChild.layer.opacity = 0.0; } else { RCTLogError(@"Unsupported layout animation createConfig property %@", deleteAnimation.property); } } withCompletionBlock:completion]; } } RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag) { RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag]; RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag); [self _purgeChildren:(NSArray> *)rootShadowView.reactSubviews fromRegistry:(NSMutableDictionary> *)_shadowViewRegistry]; [_shadowViewRegistry removeObjectForKey:rootReactTag]; [_rootViewTags removeObject:rootReactTag]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ RCTAssertMainQueue(); UIView *rootView = viewRegistry[rootReactTag]; [uiManager _purgeChildren:(NSArray> *)rootView.reactSubviews fromRegistry:(NSMutableDictionary> *)viewRegistry]; [(NSMutableDictionary *)viewRegistry removeObjectForKey:rootReactTag]; [[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerDidRemoveRootViewNotification object:uiManager userInfo:@{RCTUIManagerRootViewKey: rootView}]; }]; } RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag withView:(nonnull NSNumber *)newReactTag) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTAssert(shadowView != nil, @"shadowView (for ID %@) not found", reactTag); RCTShadowView *superShadowView = shadowView.superview; if (!superShadowView) { RCTAssert(NO, @"shadowView super (of ID %@) not found", reactTag); return; } NSUInteger indexOfView = [superShadowView.reactSubviews 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]; } RCT_EXPORT_METHOD(setChildren:(nonnull NSNumber *)containerTag reactTags:(NSArray *)reactTags) { RCTSetChildren(containerTag, reactTags, (NSDictionary> *)_shadowViewRegistry); [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry){ RCTSetChildren(containerTag, reactTags, (NSDictionary> *)viewRegistry); }]; } static void RCTSetChildren(NSNumber *containerTag, NSArray *reactTags, NSDictionary> *registry) { id container = registry[containerTag]; NSInteger index = 0; for (NSNumber *reactTag in reactTags) { id view = registry[reactTag]; if (view) { [container insertReactSubview:view atIndex:index++]; } } } RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices) { [self _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags addAtIndices:addAtIndices removeAtIndices:removeAtIndices registry:(NSMutableDictionary> *)_shadowViewRegistry]; [self addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry){ [uiManager _manageChildren:containerTag moveFromIndices:moveFromIndices moveToIndices:moveToIndices addChildReactTags:addChildReactTags addAtIndices:addAtIndices removeAtIndices:removeAtIndices registry:(NSMutableDictionary> *)viewRegistry]; }]; } - (void)_manageChildren:(NSNumber *)containerTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices addChildReactTags:(NSArray *)addChildReactTags addAtIndices:(NSArray *)addAtIndices removeAtIndices:(NSArray *)removeAtIndices registry:(NSMutableDictionary> *)registry { id container = registry[containerTag]; RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); // Removes (both permanent and temporary moves) are using "before" indices NSArray> *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; NSArray> *temporarilyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:moveFromIndices]; BOOL isUIViewRegistry = ((id)registry == (id)_viewRegistry); if (isUIViewRegistry && _layoutAnimation.deleteAnimation) { [self _removeChildren:(NSArray *)permanentlyRemovedChildren fromContainer:(UIView *)container withAnimation:_layoutAnimation]; } else { [self _removeChildren:permanentlyRemovedChildren fromContainer:container]; } [self _removeChildren:temporarilyRemovedChildren fromContainer:container]; [self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry]; // Figure out what to insert - merge temporary inserts and adds NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary]; for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) { destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index]; } for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) { id view = registry[addChildReactTags[index]]; if (view) { destinationsToChildrenToAdd[addAtIndices[index]] = view; } } NSArray *sortedIndices = [destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)]; for (NSNumber *reactIndex in sortedIndices) { NSInteger insertAtIndex = reactIndex.integerValue; // When performing a delete animation, views are not removed immediately // from their container so we need to offset the insertion index if a view // that will be removed appears earlier than the view we are inserting. if (isUIViewRegistry && _viewsToBeDeleted.count > 0) { for (NSInteger index = 0; index < insertAtIndex; index++) { UIView *subview = ((UIView *)container).reactSubviews[index]; if ([_viewsToBeDeleted containsObject:subview]) { insertAtIndex++; } } } [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:insertAtIndex]; } } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag viewName:(NSString *)viewName rootTag:(__unused NSNumber *)rootTag props:(NSDictionary *)props) { RCTComponentData *componentData = _componentDataByName[viewName]; if (componentData == nil) { RCTLogError(@"No component found for view with name \"%@\"", viewName); } // Register shadow view RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag]; if (shadowView) { [componentData setProps:props forShadowView:shadowView]; _shadowViewRegistry[reactTag] = shadowView; } // 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; // Dispatch view creation directly to the main thread instead of adding to // UIBlocks array. This way, it doesn't get deferred until after layout. __weak RCTUIManager *weakManager = self; dispatch_async(dispatch_get_main_queue(), ^{ RCTUIManager *uiManager = weakManager; if (!uiManager) { return; } UIView *view = [componentData createViewWithTag:reactTag]; if (view) { [componentData setProps:props forView:view]; // Must be done before bgColor to prevent wrong default if ([view respondsToSelector:@selector(setBackgroundColor:)]) { ((UIView *)view).backgroundColor = backgroundColor; } if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { [uiManager->_bridgeTransactionListeners addObject:view]; } uiManager->_viewRegistry[reactTag] = view; #if RCT_DEV [view _DEBUG_setReactShadowView:shadowView]; #endif } }); } RCT_EXPORT_METHOD(updateView:(nonnull NSNumber *)reactTag viewName:(NSString *)viewName // not always reliable, use shadowView.viewName if available props:(NSDictionary *)props) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTComponentData *componentData = _componentDataByName[shadowView.viewName ?: viewName]; [componentData setProps:props forShadowView:shadowView]; [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[reactTag]; [componentData setProps:props forView:view]; }]; } - (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag viewName:(NSString *)viewName props:(NSDictionary *)props { RCTAssertMainQueue(); RCTComponentData *componentData = _componentDataByName[viewName]; UIView *view = _viewRegistry[reactTag]; [componentData setProps:props forView:view]; } RCT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *newResponder = viewRegistry[reactTag]; [newResponder reactFocus]; }]; } RCT_EXPORT_METHOD(blur:(nonnull NSNumber *)reactTag) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry){ UIView *currentResponder = viewRegistry[reactTag]; [currentResponder reactBlur]; }]; } RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)point callback:(RCTResponseSenderBlock)callback) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[reactTag]; UIView *target = [view hitTest:point withEvent:nil]; CGRect frame = [target convertRect:target.bounds toView:view]; while (target.reactTag == nil && target.superview != nil) { target = target.superview; } callback(@[ RCTNullIfNil(target.reactTag), @(frame.origin.x), @(frame.origin.y), @(frame.size.width), @(frame.size.height), ]); }]; } RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag commandID:(NSInteger)commandID commandArgs:(NSArray *)commandArgs) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTComponentData *componentData = _componentDataByName[shadowView.viewName]; Class managerClass = componentData.managerClass; RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)]; id method = moduleData.methods[commandID]; NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs]; [method invokeWithBridge:_bridge module:componentData.manager arguments:args]; } - (void)partialBatchDidFlush { if (self.unsafeFlushUIChangesBeforeBatchEnds) { [self flushUIBlocks]; } } - (void)batchDidComplete { [self _layoutAndMount]; } /** * Sets up animations, computes layout, creates UI mounting blocks for computed layout, * runs these blocks and all other already existing blocks. */ - (void)_layoutAndMount { // 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 (RCTComponentData *componentData in _componentDataByName.allValues) { RCTViewManagerUIBlock uiBlock = [componentData uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry]; [self addUIBlock:uiBlock]; } [_observerCoordinator uiManagerWillPerformLayout:self]; // Perform layout for (NSNumber *reactTag in _rootViewTags) { RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag]; [self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; } [_observerCoordinator uiManagerDidPerformLayout:self]; // Properies propagation for (NSNumber *reactTag in _rootViewTags) { RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag]; [self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:rootView]; } [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { /** * TODO(tadeu): Remove it once and for all */ for (id node in uiManager->_bridgeTransactionListeners) { [node reactBridgeDidFinishTransaction]; } }]; [_observerCoordinator uiManagerWillFlushUIBlocks:self]; [self flushUIBlocks]; } - (void)flushUIBlocks { RCTAssertThread(RCTGetUIManagerQueue(), @"flushUIBlocks can only be called from the shadow queue"); // First copy the previous blocks into a temporary variable, then reset the // pending blocks to a new array. This guards against mutation while // processing the pending blocks in another thread. NSArray *previousPendingUIBlocks = _pendingUIBlocks; _pendingUIBlocks = [NSMutableArray new]; if (previousPendingUIBlocks.count) { // Execute the previously queued UI blocks RCTProfileBeginFlowEvent(); dispatch_async(dispatch_get_main_queue(), ^{ RCTProfileEndFlowEvent(); RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[UIManager flushUIBlocks]", (@{ @"count": [@(previousPendingUIBlocks.count) stringValue], })); @try { for (RCTViewManagerUIBlock block in previousPendingUIBlocks) { block(self, self->_viewRegistry); } } @catch (NSException *exception) { RCTLogError(@"Exception thrown while executing UI block: %@", exception); } RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); }); } } - (void)setNeedsLayout { // If there is an active batch layout will happen when batch finished, so we will wait for that. // Otherwise we immidiately trigger layout. if (![_bridge isBatchActive] && ![_bridge isLoading]) { [self _layoutAndMount]; } } RCT_EXPORT_METHOD(measure:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { // this view was probably collapsed out RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); callback(@[]); return; } // If in a , rootView will be the root of the modal container. UIView *rootView = view; while (rootView.superview && ![rootView isReactRootView]) { rootView = rootView.superview; } // By convention, all coordinates, whether they be touch coordinates, or // measurement coordinates are with respect to the root view. CGRect frame = view.frame; CGRect globalBounds = [view convertRect:view.bounds toView:rootView]; callback(@[ @(frame.origin.x), @(frame.origin.y), @(globalBounds.size.width), @(globalBounds.size.height), @(globalBounds.origin.x), @(globalBounds.origin.y), ]); }]; } RCT_EXPORT_METHOD(measureInWindow:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { UIView *view = viewRegistry[reactTag]; if (!view) { // this view was probably collapsed out RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); callback(@[]); return; } // Return frame coordinates in window CGRect windowFrame = [view.window convertRect:view.frame fromView:view.superview]; callback(@[ @(windowFrame.origin.x), @(windowFrame.origin.y), @(windowFrame.size.width), @(windowFrame.size.height), ]); }]; } /** * Returs if the shadow view provided has the `ancestor` shadow view as * an actual ancestor. */ RCT_EXPORT_METHOD(viewIsDescendantOf:(nonnull NSNumber *)reactTag ancestor:(nonnull NSNumber *)ancestorReactTag callback:(RCTResponseSenderBlock)callback) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag]; if (!shadowView) { return; } if (!ancestorShadowView) { return; } BOOL viewIsAncestor = [shadowView viewIsDescendantOf:ancestorShadowView]; callback(@[@(viewIsAncestor)]); } static void RCTMeasureLayout(RCTShadowView *view, RCTShadowView *ancestor, RCTResponseSenderBlock callback) { if (!view) { return; } if (!ancestor) { return; } CGRect result = [view measureLayoutRelativeToAncestor:ancestor]; if (CGRectIsNull(result)) { RCTLogError(@"view %@ (tag #%@) is not a descendant of %@ (tag #%@)", view, view.reactTag, ancestor, ancestor.reactTag); return; } CGFloat leftOffset = result.origin.x; CGFloat topOffset = result.origin.y; CGFloat width = result.size.width; CGFloat height = result.size.height; if (isnan(leftOffset) || isnan(topOffset) || isnan(width) || isnan(height)) { RCTLogError(@"Attempted to measure layout but offset or dimensions were NaN"); return; } callback(@[@(leftOffset), @(topOffset), @(width), @(height)]); } /** * Returns the computed recursive offset layout in a dictionary form. The * returned values are relative to the `ancestor` shadow view. Returns `nil`, if * the `ancestor` shadow view is not actually an `ancestor`. Does not touch * anything on the main UI thread. Invokes supplied callback with (x, y, width, * height). */ RCT_EXPORT_METHOD(measureLayout:(nonnull NSNumber *)reactTag relativeTo:(nonnull NSNumber *)ancestorReactTag errorCallback:(__unused RCTResponseSenderBlock)errorCallback callback:(RCTResponseSenderBlock)callback) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag]; RCTMeasureLayout(shadowView, ancestorShadowView, callback); } /** * Returns the computed recursive offset layout in a dictionary form. The * returned values are relative to the `ancestor` shadow view. Returns `nil`, if * the `ancestor` shadow view is not actually an `ancestor`. Does not touch * anything on the main UI thread. Invokes supplied callback with (x, y, width, * height). */ RCT_EXPORT_METHOD(measureLayoutRelativeToParent:(nonnull NSNumber *)reactTag errorCallback:(__unused RCTResponseSenderBlock)errorCallback callback:(RCTResponseSenderBlock)callback) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTMeasureLayout(shadowView, shadowView.reactSuperview, callback); } /** * Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews * that are immediate descendants to the parent view found within a specified rect. The dictionary result * contains left, top, width, height and an index. The index specifies the position among the other subviews. * Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the * passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts. */ RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect parentView:(nonnull NSNumber *)reactTag errorCallback:(__unused RCTResponseSenderBlock)errorCallback callback:(RCTResponseSenderBlock)callback) { RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; if (!shadowView) { RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag); return; } NSArray *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 descendant of %@ (tag #%@)", childShadowView, childShadowView.reactTag, shadowView, shadowView.reactTag); return; } CGFloat leftOffset = childLayout.origin.x; CGFloat topOffset = childLayout.origin.y; CGFloat width = childLayout.size.width; CGFloat height = childLayout.size.height; if (leftOffset <= rect.origin.x + rect.size.width && leftOffset + width >= rect.origin.x && topOffset <= rect.origin.y + rect.size.height && topOffset + height >= rect.origin.y) { // This view is within the layout rect NSDictionary *result = @{@"index": @(idx), @"left": @(leftOffset), @"top": @(topOffset), @"width": @(width), @"height": @(height)}; [results addObject:result]; } }]; callback(@[results]); } RCT_EXPORT_METHOD(takeSnapshot:(id /* NSString or NSNumber */)target withOptions:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { // Get view UIView *view; if (target == nil || [target isEqual:@"window"]) { view = RCTKeyWindow(); } else if ([target isKindOfClass:[NSNumber class]]) { view = viewRegistry[target]; if (!view) { RCTLogError(@"No view found with reactTag: %@", target); return; } } // Get options CGSize size = [RCTConvert CGSize:options]; NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"]; // Capture image if (size.width < 0.1 || size.height < 0.1) { size = view.bounds.size; } UIGraphicsBeginImageContextWithOptions(size, NO, 0); BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); if (!success || !image) { reject(RCTErrorUnspecified, @"Failed to capture view snapshot.", nil); return; } // Convert image to data (on a background thread) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSData *data; if ([format isEqualToString:@"png"]) { data = UIImagePNGRepresentation(image); } else if ([format isEqualToString:@"jpeg"]) { CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1]; data = UIImageJPEGRepresentation(image, quality); } else { RCTLogError(@"Unsupported image format: %@", format); return; } // Save to a temp file NSError *error = nil; NSString *tempFilePath = RCTTempFilePath(format, &error); if (tempFilePath) { if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) { resolve(tempFilePath); return; } } // If we reached here, something went wrong reject(RCTErrorUnspecified, error.localizedDescription, error); }); }]; } /** * JS sets what *it* considers to be the responder. Later, scroll views can use * this in order to determine if scrolling is appropriate. */ RCT_EXPORT_METHOD(setJSResponder:(nonnull NSNumber *)reactTag blockNativeResponder:(__unused BOOL)blockNativeResponder) { [self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { _jsResponder = viewRegistry[reactTag]; if (!_jsResponder) { RCTLogError(@"Invalid view set to be the JS responder - tag %zd", reactTag); } }]; } RCT_EXPORT_METHOD(clearJSResponder) { [self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { _jsResponder = nil; }]; } - (NSDictionary *)constantsToExport { NSMutableDictionary *constants = [NSMutableDictionary new]; NSMutableDictionary *directEvents = [NSMutableDictionary new]; NSMutableDictionary *bubblingEvents = [NSMutableDictionary new]; [_componentDataByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) { NSMutableDictionary *moduleConstants = [NSMutableDictionary new]; // Add manager class moduleConstants[@"Manager"] = RCTBridgeModuleNameForClass(componentData.managerClass); // Add native props NSDictionary *viewConfig = [componentData viewConfig]; moduleConstants[@"NativeProps"] = viewConfig[@"propTypes"]; // Add direct events for (NSString *eventName in viewConfig[@"directEvents"]) { if (!directEvents[eventName]) { directEvents[eventName] = @{ @"registrationName": [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"], }; } if (RCT_DEBUG && bubblingEvents[eventName]) { RCTLogError(@"Component '%@' re-registered bubbling event '%@' as a " "direct event", componentData.name, eventName); } } // Add bubbling events for (NSString *eventName in viewConfig[@"bubblingEvents"]) { if (!bubblingEvents[eventName]) { NSString *bubbleName = [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"]; bubblingEvents[eventName] = @{ @"phasedRegistrationNames": @{ @"bubbled": bubbleName, @"captured": [bubbleName stringByAppendingString:@"Capture"], } }; } if (RCT_DEBUG && directEvents[eventName]) { RCTLogError(@"Component '%@' re-registered direct event '%@' as a " "bubbling event", componentData.name, eventName); } } RCTAssert(!constants[name], @"UIManager already has constants for %@", componentData.name); constants[name] = moduleConstants; }]; constants[@"customBubblingEventTypes"] = bubblingEvents; constants[@"customDirectEventTypes"] = directEvents; return constants; } RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config withCallback:(RCTResponseSenderBlock)callback errorCallback:(__unused RCTResponseSenderBlock)errorCallback) { RCTLayoutAnimation *currentAnimation = _layoutAnimation; if (currentAnimation && ![config isEqualToDictionary:currentAnimation.config]) { RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", currentAnimation.config, config); } RCTLayoutAnimation *nextLayoutAnimation = [[RCTLayoutAnimation alloc] initWithDictionary:config callback:callback]; // Set up next layout animation [self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary *viewRegistry) { uiManager->_layoutAnimation = nextLayoutAnimation; }]; } - (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView *view))completion { RCTAssertMainQueue(); RCTAssert(completion != nil, @"Attempted to resolve rootView for tag %@ without a completion block", reactTag); if (reactTag == nil) { completion(nil); return; } dispatch_async(RCTGetUIManagerQueue(), ^{ NSNumber *rootTag = [self _rootTagForReactTag:reactTag]; dispatch_async(dispatch_get_main_queue(), ^{ UIView *rootView = nil; if (rootTag != nil) { rootView = [self viewForReactTag:rootTag]; } completion(rootView); }); }); } - (NSNumber *)_rootTagForReactTag:(NSNumber *)reactTag { RCTAssert(!RCTIsMainQueue(), @"Should be called on shadow queue"); if (reactTag == nil) { return nil; } if (RCTIsReactRootView(reactTag)) { return reactTag; } NSNumber *rootTag = nil; RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; while (shadowView) { RCTShadowView *parent = [shadowView reactSuperview]; if (!parent && RCTIsReactRootView(shadowView.reactTag)) { rootTag = shadowView.reactTag; break; } shadowView = parent; } return rootTag; } static UIView *_jsResponder; + (UIView *)JSResponder { return _jsResponder; } @end @implementation RCTUIManager (Deprecated) - (void)registerRootView:(UIView *)rootView withSizeFlexibility:(__unused RCTRootViewSizeFlexibility)sizeFlexibility { RCTLogWarn(@"Calling of `[-RCTUIManager registerRootView:withSizeFlexibility:]` which is deprecated."); [self registerRootView:rootView]; } - (void)setFrame:(CGRect)frame forView:(UIView *)view { RCTLogWarn(@"Calling of `[-RCTUIManager setFrame:forView:]` which is deprecated."); [self setSize:frame.size forView:view]; } RCT_EXPORT_METHOD(getContentSizeMultiplier:(nonnull RCTResponseSenderBlock)callback) { RCTLogWarn(@"`getContentSizeMultiplier` is deprecated. Instead, use `PixelRatio.getFontScale()` and listen to the `didUpdateDimensions` event."); callback(@[@(_bridge.accessibilityManager.multiplier)]); } @end @implementation RCTBridge (RCTUIManager) - (RCTUIManager *)uiManager { return [self moduleForClass:[RCTUIManager class]]; } @end