mirror of
https://github.com/status-im/react-native.git
synced 2025-02-05 06:04:15 +00:00
Fix LayoutAnimation iOS delete bug when adding and removing views at the same time
Summary: Fix a bug that happens when views are added and removed in the same manageChildren block with a delete animation. What happens is that the inserted view is not inserted at the proper index if the deleted view index is smaller than the inserted one. This is because the view is not immediately removed from the subviews array so we need to offset the insert index for each view that is going to be deleted with an animation and is before the inserted view. To do this I separated `_removeChildren` into 2 different functions, one for animated delete and one for normal delete. The animated one returns an array of `RCTComponent` that are going to be animated. We can then use this array to offset the insert index. **Test plan (required)** Tested that this fixed the bug in an app where I noticed it, also tested the UIExplorer example to make sure LayoutAnimations still worked properly. Closes https://github.com/facebook/react-native/pull/7942 Differential Revision: D3417194 Pulled By: nicklockwood fbshipit-source-id: 790f4ac15a8552323b359e6466cecfa80418c63c
This commit is contained in:
parent
c03b166854
commit
6236a593d8
@ -210,6 +210,7 @@ static UIViewAnimationOptions UIViewAnimationOptionsFromRCTAnimationType(RCTAnim
|
|||||||
|
|
||||||
// Animation
|
// Animation
|
||||||
RCTLayoutAnimation *_layoutAnimation; // Main thread only
|
RCTLayoutAnimation *_layoutAnimation; // Main thread only
|
||||||
|
NSMutableSet<UIView *> *_viewsToBeDeleted; // Main thread only
|
||||||
|
|
||||||
NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
|
NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
|
||||||
NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only
|
NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only
|
||||||
@ -318,6 +319,8 @@ RCT_EXPORT_MODULE()
|
|||||||
|
|
||||||
_bridgeTransactionListeners = [NSMutableSet new];
|
_bridgeTransactionListeners = [NSMutableSet new];
|
||||||
|
|
||||||
|
_viewsToBeDeleted = [NSMutableSet new];
|
||||||
|
|
||||||
// Get view managers from bridge
|
// Get view managers from bridge
|
||||||
NSMutableDictionary *componentDataByName = [NSMutableDictionary new];
|
NSMutableDictionary *componentDataByName = [NSMutableDictionary new];
|
||||||
for (Class moduleClass in _bridge.moduleClasses) {
|
for (Class moduleClass in _bridge.moduleClasses) {
|
||||||
@ -339,7 +342,6 @@ RCT_EXPORT_MODULE()
|
|||||||
selector:@selector(interfaceOrientationWillChange:)
|
selector:@selector(interfaceOrientationWillChange:)
|
||||||
name:UIApplicationWillChangeStatusBarOrientationNotification
|
name:UIApplicationWillChangeStatusBarOrientationNotification
|
||||||
object:nil];
|
object:nil];
|
||||||
|
|
||||||
[RCTAnimation initializeStatics];
|
[RCTAnimation initializeStatics];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,53 +780,61 @@ RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containe
|
|||||||
|
|
||||||
- (void)_removeChildren:(NSArray<id<RCTComponent>> *)children
|
- (void)_removeChildren:(NSArray<id<RCTComponent>> *)children
|
||||||
fromContainer:(id<RCTComponent>)container
|
fromContainer:(id<RCTComponent>)container
|
||||||
permanent:(BOOL)permanent
|
|
||||||
{
|
{
|
||||||
RCTLayoutAnimation *layoutAnimation = _layoutAnimation;
|
for (id<RCTComponent> removedChild in children) {
|
||||||
RCTAnimation *deleteAnimation = layoutAnimation.deleteAnimation;
|
[container removeReactSubview:removedChild];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove subviews from their parent with an animation.
|
||||||
|
*/
|
||||||
|
- (void)_removeChildren:(NSArray<UIView *> *)children
|
||||||
|
fromContainer:(UIView *)container
|
||||||
|
withAnimation:(RCTLayoutAnimation *)animation
|
||||||
|
{
|
||||||
|
RCTAssertMainQueue();
|
||||||
|
RCTAnimation *deleteAnimation = animation.deleteAnimation;
|
||||||
|
|
||||||
__block NSUInteger completionsCalled = 0;
|
__block NSUInteger completionsCalled = 0;
|
||||||
|
for (UIView *removedChild in children) {
|
||||||
for (id<RCTComponent> removedChild in children) {
|
|
||||||
|
|
||||||
void (^completion)(BOOL) = ^(BOOL finished) {
|
void (^completion)(BOOL) = ^(BOOL finished) {
|
||||||
completionsCalled++;
|
completionsCalled++;
|
||||||
|
|
||||||
|
[_viewsToBeDeleted removeObject:removedChild];
|
||||||
[container removeReactSubview:removedChild];
|
[container removeReactSubview:removedChild];
|
||||||
|
|
||||||
if (layoutAnimation.callback && completionsCalled == children.count) {
|
if (animation.callback && completionsCalled == children.count) {
|
||||||
layoutAnimation.callback(@[@(finished)]);
|
animation.callback(@[@(finished)]);
|
||||||
|
|
||||||
// It's unsafe to call this callback more than once, so we nil it out here
|
// It's unsafe to call this callback more than once, so we nil it out here
|
||||||
// to make sure that doesn't happen.
|
// to make sure that doesn't happen.
|
||||||
layoutAnimation.callback = nil;
|
animation.callback = nil;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (permanent && deleteAnimation && [removedChild isKindOfClass: [UIView class]]) {
|
[_viewsToBeDeleted addObject:removedChild];
|
||||||
UIView *view = (UIView *)removedChild;
|
|
||||||
|
|
||||||
// Disable user interaction while the view is animating since JS won't receive
|
// Disable user interaction while the view is animating since JS won't receive
|
||||||
// the view events anyway.
|
// the view events anyway.
|
||||||
view.userInteractionEnabled = NO;
|
removedChild.userInteractionEnabled = NO;
|
||||||
|
|
||||||
NSString *property = deleteAnimation.property;
|
NSString *property = deleteAnimation.property;
|
||||||
[deleteAnimation performAnimations:^{
|
[deleteAnimation performAnimations:^{
|
||||||
if ([property isEqualToString:@"scaleXY"]) {
|
if ([property isEqualToString:@"scaleXY"]) {
|
||||||
view.layer.transform = CATransform3DMakeScale(0, 0, 0);
|
removedChild.layer.transform = CATransform3DMakeScale(0, 0, 0);
|
||||||
} else if ([property isEqualToString:@"opacity"]) {
|
} else if ([property isEqualToString:@"opacity"]) {
|
||||||
view.layer.opacity = 0.0;
|
removedChild.layer.opacity = 0.0;
|
||||||
} else {
|
} else {
|
||||||
RCTLogError(@"Unsupported layout animation createConfig property %@",
|
RCTLogError(@"Unsupported layout animation createConfig property %@",
|
||||||
deleteAnimation.property);
|
deleteAnimation.property);
|
||||||
}
|
}
|
||||||
} withCompletionBlock:completion];
|
} withCompletionBlock:completion];
|
||||||
} else {
|
|
||||||
[container removeReactSubview:removedChild];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag)
|
RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag)
|
||||||
{
|
{
|
||||||
RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag];
|
RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag];
|
||||||
@ -938,13 +948,19 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
|
|||||||
[self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
|
[self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
|
||||||
NSArray<id<RCTComponent>> *temporarilyRemovedChildren =
|
NSArray<id<RCTComponent>> *temporarilyRemovedChildren =
|
||||||
[self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
|
[self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
|
||||||
[self _removeChildren:permanentlyRemovedChildren fromContainer:container permanent:true];
|
|
||||||
[self _removeChildren:temporarilyRemovedChildren fromContainer:container permanent:false];
|
|
||||||
|
|
||||||
|
BOOL isUIViewRegistry = ((id)registry == (id)_viewRegistry);
|
||||||
|
if (isUIViewRegistry && _layoutAnimation.deleteAnimation) {
|
||||||
|
[self _removeChildren:(NSArray<UIView *> *)permanentlyRemovedChildren
|
||||||
|
fromContainer:(UIView *)container
|
||||||
|
withAnimation:_layoutAnimation];
|
||||||
|
} else {
|
||||||
|
[self _removeChildren:permanentlyRemovedChildren fromContainer:container];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self _removeChildren:temporarilyRemovedChildren fromContainer:container];
|
||||||
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
|
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
|
||||||
|
|
||||||
// TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient
|
|
||||||
|
|
||||||
// Figure out what to insert - merge temporary inserts and adds
|
// Figure out what to insert - merge temporary inserts and adds
|
||||||
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
|
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
|
||||||
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
|
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
|
||||||
@ -960,8 +976,22 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
|
|||||||
NSArray<NSNumber *> *sortedIndices =
|
NSArray<NSNumber *> *sortedIndices =
|
||||||
[destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
[destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)];
|
||||||
for (NSNumber *reactIndex in sortedIndices) {
|
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]
|
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex]
|
||||||
atIndex:reactIndex.integerValue];
|
atIndex:insertAtIndex];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user