Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native Animated - Restore default values when removing props on iOS #11819

Closed
38 changes: 30 additions & 8 deletions Libraries/NativeAnimation/Nodes/RCTPropsAnimatedNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@
#import "RCTStyleAnimatedNode.h"
#import "RCTValueAnimatedNode.h"

@implementation RCTPropsAnimatedNode {
@implementation RCTPropsAnimatedNode
{
NSNumber *_connectedViewTag;
NSString *_connectedViewName;
RCTUIManager *_uiManager;
NSMutableDictionary<NSString *, NSObject *> *_propsDictionary;
}

- (instancetype)initWithTag:(NSNumber *)tag
config:(NSDictionary<NSString *, id> *)config;
{
if ((self = [super initWithTag:tag config:config])) {
_propsDictionary = [NSMutableDictionary new];
}
return self;
}

- (void)connectToView:(NSNumber *)viewTag
Expand All @@ -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;
Expand All @@ -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];
}
}

Expand Down
3 changes: 2 additions & 1 deletion Libraries/NativeAnimation/RCTNativeAnimatedModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTUIManager.h>

#import "RCTValueAnimatedNode.h"

@interface RCTNativeAnimatedModule : RCTEventEmitter <RCTBridgeModule, RCTValueAnimatedNodeObserver, RCTEventDispatcherObserver>
@interface RCTNativeAnimatedModule : RCTEventEmitter <RCTBridgeModule, RCTValueAnimatedNodeObserver, RCTEventDispatcherObserver, RCTUIManagerObserver>

@end
84 changes: 57 additions & 27 deletions Libraries/NativeAnimation/RCTNativeAnimatedModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,27 @@
@implementation RCTNativeAnimatedModule
{
RCTNativeAnimatedNodesManager *_nodesManager;

// Oparations called after views have been updated.
NSMutableArray<AnimatedOperation> *_operations;
// Operations called before views have been updated.
NSMutableArray<AnimatedOperation> *_preOperations;
}

RCT_EXPORT_MODULE();

- (void)invalidate
{
[_nodesManager stopAnimationLoop];
}

- (void)dealloc
{
[self.bridge.eventDispatcher removeDispatchObserver:self];
[self.bridge.uiManager removeUIManagerObserver:self];
}

- (dispatch_queue_t)methodQueue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment saying this needs to be the UIManager queue because we access some data structures in uiManagerWillFlushUIBlocks which is also called on that queue

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the only reason we need to run on the UIManager queue now is to avoid having to lock _operations and _preOperations.. Added a comment to explain that.

{
// 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();
}

Expand All @@ -41,32 +45,34 @@ - (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

RCT_EXPORT_METHOD(createAnimatedNode:(nonnull NSNumber *)tag
config:(NSDictionary<NSString *, id> *)config)
{
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager createAnimatedNode:tag config:config];
}];
}

RCT_EXPORT_METHOD(connectAnimatedNodes:(nonnull NSNumber *)parentTag
childTag:(nonnull NSNumber *)childTag)
{
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager connectAnimatedNodes:parentTag childTag:childTag];
}];
}

RCT_EXPORT_METHOD(disconnectAnimatedNodes:(nonnull NSNumber *)parentTag
childTag:(nonnull NSNumber *)childTag)
{
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager disconnectAnimatedNodes:parentTag childTag:childTag];
}];
}
Expand All @@ -76,44 +82,44 @@ - (void)setBridge:(RCTBridge *)bridge
config:(NSDictionary<NSString *, id> *)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];
}];
}

RCT_EXPORT_METHOD(setAnimatedNodeValue:(nonnull NSNumber *)nodeTag
value:(nonnull NSNumber *)value)
{
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager setAnimatedNodeValue:nodeTag value:value];
}];
}

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];
}];
}
Expand All @@ -122,38 +128,41 @@ - (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];
}];
}

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];
}];
}

RCT_EXPORT_METHOD(startListeningToAnimatedNodeValue:(nonnull NSNumber *)tag)
{
__weak id<RCTValueAnimatedNodeObserver> valueObserver = self;
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager startListeningToAnimatedNodeValue:tag valueObserver:valueObserver];
}];
}

RCT_EXPORT_METHOD(stopListeningToAnimatedNodeValue:(nonnull NSNumber *)tag)
{
__weak id<RCTValueAnimatedNodeObserver> valueObserver = self;
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager stopListeningToAnimatedNodeValue:tag valueObserver:valueObserver];
}];
}
Expand All @@ -162,7 +171,7 @@ - (void)setBridge:(RCTBridge *)bridge
eventName:(nonnull NSString *)eventName
eventMapping:(NSDictionary<NSString *, id> *)eventMapping)
{
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
[self addOperationBlock:^(RCTNativeAnimatedNodesManager *nodesManager) {
[nodesManager addAnimatedEventToView:viewTag eventName:eventName eventMapping:eventMapping];
}];
}
Expand All @@ -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<AnimatedOperation> *preOperations = _preOperations;
NSArray<AnimatedOperation> *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<NSNumber *, UIView *> *viewRegistry) {
for (AnimatedOperation operation in preOperations) {
operation(self->_nodesManager);
}];
[self->_nodesManager updateAnimations];
});
}
}];

[uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
for (AnimatedOperation operation in operations) {
operation(self->_nodesManager);
}
}];
}

#pragma mark -- Events
Expand Down
34 changes: 34 additions & 0 deletions React/Modules/RCTUIManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <NSObject>

/**
* 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;

/**
Expand Down Expand Up @@ -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<RCTUIManagerObserver>)observer;

/**
* Remove a UIManagerObserver. This method can be called safely from any queue.
*/
- (void)removeUIManagerObserver:(id<RCTUIManagerObserver>)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
Expand Down