Permalink
Browse files

Native Animated - Restore default values when removing props on iOS

Summary:
This fixes a bug that causes properties to keep stale values because they were not restored to their default after being removed when their value was controlled by native animated.

To fix this we restore default values in `disconnectFromView` by updating views with null values for all props that we modified previously. However this causes another issue where we lose any props that were set by the normal process because NativeAnimated operations are always executed after UIManager operatations. To fix this I added a way to hook into UIManager view updating process to be able to execute NativeAnimated operations either before or after updating native views.

In the case of disconnecting we want to do it before updating views so that it does: Value changed by native animated -> value restored to default -> (optional) value updated by normal prop.

This PR also depends on #10658.

**Test plan**
Tested that this fixed a particular bug in an app that uses ex-navigation + native animations where a navbar w
Closes #11819

Differential Revision: D4752566

Pulled By: javache

fbshipit-source-id: 68ee28200ffeba859ae1b98ac753bd7dcb8910f0
  • Loading branch information...
janicduplessis authored and facebook-github-bot committed Mar 28, 2017
1 parent acc1edd commit c9fae2fb935c7b7b619b246f42b3fb9d3c14c9ac
@@ -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
@@ -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;
@@ -41,7 +63,7 @@ - (void)disconnectFromView:(NSNumber *)viewTag
- (NSString *)propertyNameForParentTag:(NSNumber *)parentTag
{
__block NSString *propertyName;
[self.config[@"props"] enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull property, NSNumber * _Nonnull tag, BOOL * _Nonnull stop) {
[self.config[@"props"] enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull property, NSNumber *_Nonnull tag, BOOL *_Nonnull stop) {
if ([tag isEqualToNumber:parentTag]) {
propertyName = property;
*stop = YES;
@@ -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) {
[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];
}
}
@@ -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
@@ -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
{
// 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,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];
}];
}
@@ -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];
}];
}
@@ -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];
}];
}
@@ -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];
}];
}
@@ -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
@@ -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;
/**
@@ -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
Oops, something went wrong.

0 comments on commit c9fae2f

Please sign in to comment.