diff --git a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m index 76b146e525e07a..a1424a23519149 100644 --- a/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m +++ b/Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m @@ -16,10 +16,21 @@ #import "RCTStyleAnimatedNode.h" #import "RCTValueAnimatedNode.h" -@implementation RCTPropsAnimatedNode { +@implementation RCTPropsAnimatedNode +{ NSNumber *_connectedViewTag; NSString *_connectedViewName; RCTUIManager *_uiManager; + NSMutableDictionary *_propsDictionary; +} + +- (instancetype)initWithTag:(NSNumber *)tag + config:(NSDictionary *)config; +{ + if ((self = [super initWithTag:tag config:config])) { + _propsDictionary = [NSMutableDictionary new]; + } + return self; } - (void)connectToView:(NSNumber *)viewTag @@ -33,6 +44,17 @@ - (void)connectToView:(NSNumber *)viewTag - (void)disconnectFromView:(NSNumber *)viewTag { + // Restore the default value for all props that were modified by this node. + for (NSString *key in _propsDictionary.allKeys) { + _propsDictionary[key] = [NSNull null]; + } + + if (_propsDictionary.count) { + [_uiManager synchronouslyUpdateViewOnUIThread:_connectedViewTag + viewName:_connectedViewName + props:_propsDictionary]; + } + _connectedViewTag = nil; _connectedViewName = nil; _uiManager = nil; @@ -54,29 +76,29 @@ - (void)performUpdate { [super performUpdate]; + // Since we are updating nodes after detaching them from views there is a time where it's + // possible that the view was disconnected and still receive an update, this is normal and we can + // simply skip that update. if (!_connectedViewTag) { - RCTLogError(@"Node has not been attached to a view"); return; } - NSMutableDictionary *props = [NSMutableDictionary dictionary]; [self.parentNodes enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull parentTag, RCTAnimatedNode * _Nonnull parentNode, BOOL * _Nonnull stop) { if ([parentNode isKindOfClass:[RCTStyleAnimatedNode class]]) { - [props addEntriesFromDictionary:[(RCTStyleAnimatedNode *)parentNode propsDictionary]]; + [self->_propsDictionary addEntriesFromDictionary:[(RCTStyleAnimatedNode *)parentNode propsDictionary]]; } else if ([parentNode isKindOfClass:[RCTValueAnimatedNode class]]) { NSString *property = [self propertyNameForParentTag:parentTag]; CGFloat value = [(RCTValueAnimatedNode *)parentNode value]; - [props setObject:@(value) forKey:property]; + self->_propsDictionary[property] = @(value); } - }]; - if (props.count) { + if (_propsDictionary.count) { [_uiManager synchronouslyUpdateViewOnUIThread:_connectedViewTag viewName:_connectedViewName - props:props]; + props:_propsDictionary]; } } diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h index 4bc7831b1b9ade..c6579438f5a519 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.h +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.h @@ -10,9 +10,10 @@ #import #import #import +#import #import "RCTValueAnimatedNode.h" -@interface RCTNativeAnimatedModule : RCTEventEmitter +@interface RCTNativeAnimatedModule : RCTEventEmitter @end diff --git a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m index 4b30c47d7c419c..26cfbf567fc934 100644 --- a/Libraries/NativeAnimation/RCTNativeAnimatedModule.m +++ b/Libraries/NativeAnimation/RCTNativeAnimatedModule.m @@ -15,7 +15,11 @@ @implementation RCTNativeAnimatedModule { RCTNativeAnimatedNodesManager *_nodesManager; + + // Oparations called after views have been updated. NSMutableArray *_operations; + // Operations called before views have been updated. + NSMutableArray *_preOperations; } RCT_EXPORT_MODULE(); @@ -23,15 +27,15 @@ @implementation RCTNativeAnimatedModule - (void)invalidate { [_nodesManager stopAnimationLoop]; -} - -- (void)dealloc -{ [self.bridge.eventDispatcher removeDispatchObserver:self]; + [self.bridge.uiManager removeUIManagerObserver:self]; } - (dispatch_queue_t)methodQueue { + // This module needs to be on the same queue as the UIManager to avoid + // having to lock `_operations` and `_preOperations` since `uiManagerWillFlushUIBlocks` + // will be called from that queue. return RCTGetUIManagerQueue(); } @@ -41,8 +45,10 @@ - (void)setBridge:(RCTBridge *)bridge _nodesManager = [[RCTNativeAnimatedNodesManager alloc] initWithUIManager:self.bridge.uiManager]; _operations = [NSMutableArray new]; + _preOperations = [NSMutableArray new]; [bridge.eventDispatcher addDispatchObserver:self]; + [bridge.uiManager addUIManagerObserver:self]; } #pragma mark -- API @@ -50,7 +56,7 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(createAnimatedNode:(nonnull NSNumber *)tag config:(NSDictionary *)config) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager createAnimatedNode:tag config:config]; }]; } @@ -58,7 +64,7 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(connectAnimatedNodes:(nonnull NSNumber *)parentTag childTag:(nonnull NSNumber *)childTag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager connectAnimatedNodes:parentTag childTag:childTag]; }]; } @@ -66,7 +72,7 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(disconnectAnimatedNodes:(nonnull NSNumber *)parentTag childTag:(nonnull NSNumber *)childTag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager disconnectAnimatedNodes:parentTag childTag:childTag]; }]; } @@ -76,14 +82,14 @@ - (void)setBridge:(RCTBridge *)bridge config:(NSDictionary *)config endCallback:(RCTResponseSenderBlock)callBack) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager startAnimatingNode:animationId nodeTag:nodeTag config:config endCallback:callBack]; }]; } RCT_EXPORT_METHOD(stopAnimation:(nonnull NSNumber *)animationId) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager stopAnimation:animationId]; }]; } @@ -91,7 +97,7 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(setAnimatedNodeValue:(nonnull NSNumber *)nodeTag value:(nonnull NSNumber *)value) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager setAnimatedNodeValue:nodeTag value:value]; }]; } @@ -99,21 +105,21 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(setAnimatedNodeOffset:(nonnull NSNumber *)nodeTag offset:(nonnull NSNumber *)offset) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager setAnimatedNodeOffset:nodeTag offset:offset]; }]; } RCT_EXPORT_METHOD(flattenAnimatedNodeOffset:(nonnull NSNumber *)nodeTag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager flattenAnimatedNodeOffset:nodeTag]; }]; } RCT_EXPORT_METHOD(extractAnimatedNodeOffset:(nonnull NSNumber *)nodeTag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager extractAnimatedNodeOffset:nodeTag]; }]; } @@ -122,7 +128,7 @@ - (void)setBridge:(RCTBridge *)bridge viewTag:(nonnull NSNumber *)viewTag) { NSString *viewName = [self.bridge.uiManager viewNameForReactTag:viewTag]; - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager connectAnimatedNodeToView:nodeTag viewTag:viewTag viewName:viewName]; }]; } @@ -130,14 +136,17 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView:(nonnull NSNumber *)nodeTag viewTag:(nonnull NSNumber *)viewTag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + // Disconnecting a view also restores its default values so we have to make + // sure this happens before views get updated with their new props. This is + // why we enqueue this on the pre-operations queue. + [self addPreOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager disconnectAnimatedNodeFromView:nodeTag viewTag:viewTag]; }]; } RCT_EXPORT_METHOD(dropAnimatedNode:(nonnull NSNumber *)tag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager dropAnimatedNode:tag]; }]; } @@ -145,7 +154,7 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(startListeningToAnimatedNodeValue:(nonnull NSNumber *)tag) { __weak id valueObserver = self; - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager startListeningToAnimatedNodeValue:tag valueObserver:valueObserver]; }]; } @@ -153,7 +162,7 @@ - (void)setBridge:(RCTBridge *)bridge RCT_EXPORT_METHOD(stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag) { __weak id valueObserver = self; - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager stopListeningToAnimatedNodeValue:tag valueObserver:valueObserver]; }]; } @@ -162,7 +171,7 @@ - (void)setBridge:(RCTBridge *)bridge eventName:(nonnull NSString *)eventName eventMapping:(NSDictionary *)eventMapping) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager addAnimatedEventToView:viewTag eventName:eventName eventMapping:eventMapping]; }]; } @@ -171,24 +180,45 @@ - (void)setBridge:(RCTBridge *)bridge eventName:(nonnull NSString *)eventName animatedNodeTag:(nonnull NSNumber *)animatedNodeTag) { - [_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) { + [self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) { [nodesManager removeAnimatedEventFromView:viewTag eventName:eventName animatedNodeTag:animatedNodeTag]; }]; } #pragma mark -- Batch handling -- (void)batchDidComplete +- (void)addOperationBlock:(AnimatedOperation)operation { - NSArray *operations = _operations; + [_operations addObject:operation]; +} + +- (void)addPreOperationBlock:(AnimatedOperation)operation +{ + [_preOperations addObject:operation]; +} + +- (void)uiManagerWillFlushUIBlocks:(RCTUIManager *)uiManager +{ + if (_preOperations.count == 0 && _operations.count == 0) { + return; + } + + NSArray *preOperations = _preOperations; + NSArray *operations = _operations; + _preOperations = [NSMutableArray new]; _operations = [NSMutableArray new]; - dispatch_async(dispatch_get_main_queue(), ^{ - [operations enumerateObjectsUsingBlock:^(AnimatedOperation operation, NSUInteger i, BOOL *stop) { + [uiManager prependUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { + for (AnimatedOperation operation in preOperations) { operation(self->_nodesManager); - }]; - [self->_nodesManager updateAnimations]; - }); + } + }]; + + [uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary *viewRegistry) { + for (AnimatedOperation operation in operations) { + operation(self->_nodesManager); + } + }]; } #pragma mark -- Events diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index f1aa37b954aba4..e65f889b8a1bd3 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -48,6 +48,23 @@ RCT_EXTERN NSString *const RCTUIManagerDidRemoveRootViewNotification; */ RCT_EXTERN NSString *const RCTUIManagerRootViewKey; +@class RCTUIManager; + +/** + * Allows to hook into UIManager internals. This can be used to execute code at + * specific points during the view updating process. + */ +@protocol RCTUIManagerObserver + +/** + * Called before flushing UI blocks at the end of a batch. Note that this won't + * get called for partial batches when using `unsafeFlushUIChangesBeforeBatchEnds`. + * This is called from the UIManager queue. Can be used to add UI operations in that batch. + */ +- (void)uiManagerWillFlushUIBlocks:(RCTUIManager *)manager; + +@end + @protocol RCTScrollableProtocol; /** @@ -105,6 +122,23 @@ RCT_EXTERN NSString *const RCTUIManagerRootViewKey; */ - (void)addUIBlock:(RCTViewManagerUIBlock)block; +/** + * Schedule a block to be executed on the UI thread. Useful if you need to execute + * view logic before all currently queued view updates have completed. + */ +- (void)prependUIBlock:(RCTViewManagerUIBlock)block; + +/** + * Add a UIManagerObserver. See the RCTUIManagerObserver protocol for more info. This + * method can be called safely from any queue. + */ +- (void)addUIManagerObserver:(id)observer; + +/** + * Remove a UIManagerObserver. This method can be called safely from any queue. + */ +- (void)removeUIManagerObserver:(id)observer; + /** * Used by native animated module to bypass the process of updating the values through the shadow * view hierarchy. This method will directly update native views, which means that updates for diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 26918341bcae0b..3e665f16f708f4 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -225,6 +225,7 @@ @implementation RCTUIManager NSDictionary *_componentDataByName; NSMutableSet> *_bridgeTransactionListeners; + NSMutableSet> *_uiManagerObservers; } @synthesize bridge = _bridge; @@ -305,6 +306,7 @@ - (void)setBridge:(RCTBridge *)bridge _rootViewTags = [NSMutableSet new]; _bridgeTransactionListeners = [NSMutableSet new]; + _uiManagerObservers = [NSMutableSet new]; _viewsToBeDeleted = [NSMutableSet new]; @@ -501,6 +503,19 @@ - (void)addUIBlock:(RCTViewManagerUIBlock)block [_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"); @@ -698,6 +713,20 @@ - (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowV } } +- (void)addUIManagerObserver:(id)observer +{ + dispatch_async(RCTGetUIManagerQueue(), ^{ + [_uiManagerObservers addObject:observer]; + }); +} + +- (void)removeUIManagerObserver:(id)observer +{ + dispatch_async(RCTGetUIManagerQueue(), ^{ + [_uiManagerObservers removeObject:observer]; + }); +} + /** * A method to be called from JS, which takes a container ID and then releases * all subviews for that container upon receipt. @@ -1139,6 +1168,10 @@ - (void)_layoutAndMount } }]; + for (id observer in _uiManagerObservers) { + [observer uiManagerWillFlushUIBlocks:self]; + } + [self flushUIBlocks]; }