[React Native] Remove layout-only nodes (Take 2!)

Summary:
Remove layout-only views. Works by checking properties against a list of known properties that only affect layout. The `RCTShadowView` hierarchy still has a 1:1 correlation with the JS nodes.

This works by adjusting the tags and indices in `manageChildren`. For example, if JS told us to insert tag 1 at index 0 and tag 1 is layout-only with children whose tags are 2 and 3, we adjust it so we insert tags 2 and 3 at indices 0 and 1. This keeps changes out of `RCTView` and `RCTScrollView`. In order to simplify this logic, view moves are now processed as view removals followed by additions. A move from index 0 to 1 is recorded as a removal of view at indices 0 and 1 and an insertion of tags 1 and 2 at indices 0 and 1. Of course, the remaining indices have to be offset to take account for this.

The `collapsible` attribute is a bit of a hack to force `RCTScrollView` to always have one child. This was easier than rethinking out the logic there, but we could change this later.
This commit is contained in:
Alexsander Akers 2015-07-05 05:41:58 -02:00
parent 0f2d8e662e
commit 02db374e50
10 changed files with 922 additions and 118 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -2,6 +2,8 @@
#import <XCTest/XCTest.h>
#import "RCTRootView.h"
#import "RCTShadowView.h"
#import "RCTSparseArray.h"
#import "RCTUIManager.h"
#import "UIView+React.h"
@ -9,14 +11,36 @@
@interface RCTUIManager (Testing)
- (void)_manageChildren:(NSNumber *)containerReactTag
moveFromIndices:(NSArray *)moveFromIndices
moveToIndices:(NSArray *)moveToIndices
addChildReactTags:(NSArray *)addChildReactTags
addAtIndices:(NSArray *)addAtIndices
removeAtIndices:(NSArray *)removeAtIndices
registry:(RCTSparseArray *)registry;
- (void)modifyManageChildren:(NSNumber *)containerReactTag
addChildReactTags:(NSMutableArray *)mutableAddChildReactTags
addAtIndices:(NSMutableArray *)mutableAddAtIndices
removeAtIndices:(NSMutableArray *)mutableRemoveAtIndices;
- (void)createView:(NSNumber *)reactTag
viewName:(NSString *)viewName
rootTag:(NSNumber *)rootTag
props:(NSDictionary *)props;
- (void)updateView:(NSNumber *)reactTag
viewName:(NSString *)viewName
props:(NSDictionary *)props;
- (void)manageChildren:(NSNumber *)containerReactTag
moveFromIndices:(NSArray *)moveFromIndices
moveToIndices:(NSArray *)moveToIndices
addChildReactTags:(NSArray *)addChildReactTags
addAtIndices:(NSArray *)addAtIndices
removeAtIndices:(NSArray *)removeAtIndices;
- (void)flushUIBlocks;
@property (nonatomic, readonly) RCTSparseArray *viewRegistry;
@property (nonatomic, readonly) RCTSparseArray *shadowViewRegistry; // RCT thread only
@end
@ -39,6 +63,11 @@
UIView *registeredView = [[UIView alloc] init];
[registeredView setReactTag:@(i)];
_uiManager.viewRegistry[i] = registeredView;
RCTShadowView *registeredShadowView = [[RCTShadowView alloc] init];
registeredShadowView.viewName = @"RCTView";
[registeredShadowView setReactTag:@(i)];
_uiManager.shadowViewRegistry[i] = registeredShadowView;
}
}
@ -55,8 +84,6 @@
// Add views 1-5 to view 20
[_uiManager _manageChildren:@20
moveFromIndices:nil
moveToIndices:nil
addChildReactTags:tagsToAdd
addAtIndices:addAtIndices
removeAtIndices:nil
@ -89,8 +116,6 @@
// Remove views 1-5 from view 20
[_uiManager _manageChildren:@20
moveFromIndices:nil
moveToIndices:nil
addChildReactTags:nil
addAtIndices:nil
removeAtIndices:removeAtIndices
@ -128,11 +153,9 @@
{
UIView *containerView = _uiManager.viewRegistry[20];
NSArray *removeAtIndices = @[@2, @3, @5, @8];
NSArray *addAtIndices = @[@0, @6];
NSArray *tagsToAdd = @[@11, @12];
NSArray *moveFromIndices = @[@4, @9];
NSArray *moveToIndices = @[@1, @7];
NSArray *removeAtIndices = @[@2, @3, @5, @8, @4, @9];
NSArray *addAtIndices = @[@0, @6, @1, @7];
NSArray *tagsToAdd = @[@11, @12, @5, @10];
// We need to keep these in array to keep them around
NSMutableArray *viewsToRemove = [NSMutableArray array];
@ -148,8 +171,6 @@
}
[_uiManager _manageChildren:@20
moveFromIndices:moveFromIndices
moveToIndices:moveToIndices
addChildReactTags:tagsToAdd
addAtIndices:addAtIndices
removeAtIndices:removeAtIndices
@ -176,4 +197,329 @@
}
}
/* +-----------------------------------------------------------+ +----------------------+
* | Shadow Hierarchy | | Legend |
* +-----------------------------------------------------------+ +----------------------+
* | | | |
* | +---+ ****** | | ******************** |
* | | 1 | * 11 * | | * Layout-only View * |
* | +---+ ****** | | ******************** |
* | | | | | |
* | +-------+---+---+----------+ +---+---+ | | +----+ |
* | | | | | | | | | |View| Subview |
* | v v v v v v | | +----+ -----------> |
* | ***** +---+ ***** +---+ +----+ +----+ | | |
* | * 2 * | 3 | * 4 * | 5 | | 12 | | 13 | | +----------------------+
* | ***** +---+ ***** +---+ +----+ +----+ |
* | | | | |
* | +---+--+ | +---+---+ |
* | | | | | | |
* | v v v v v |
* | +---+ +---+ +---+ +---+ ****** |
* | | 6 | | 7 | | 8 | | 9 | * 10 * |
* | +---+ +---+ +---+ +---+ ****** |
* | |
* +-----------------------------------------------------------+
*
* +-----------------------------------------------------------+
* | View Hierarchy |
* +-----------------------------------------------------------+
* | |
* | +---+ ****** |
* | | 1 | * 11 * |
* | +---+ ****** |
* | | | |
* | +------+------+------+------+ +---+---+ |
* | | | | | | | | |
* | v v v v v v v |
* | +---+ +---+ +---+ +---+ +---+ +----+ +----+ |
* | | 6 | | 7 | | 3 | | 8 | | 5 | | 12 | | 13 | |
* | +---+ +---+ +---+ +---+ +---+ +----+ +----+ |
* | | |
* | v |
* | +---+ |
* | | 9 | |
* | +---+ |
* | |
* +-----------------------------------------------------------+
*/
- (void)updateShadowViewWithReactTag:(NSNumber *)reactTag layoutOnly:(BOOL)isLayoutOnly childTags:(NSArray *)childTags
{
RCTShadowView *shadowView = _uiManager.shadowViewRegistry[reactTag];
shadowView.allProps = isLayoutOnly ? @{} : @{@"collapsible": @NO};
[childTags enumerateObjectsUsingBlock:^(NSNumber *childTag, NSUInteger idx, __unused BOOL *stop) {
[shadowView insertReactSubview:_uiManager.shadowViewRegistry[childTag] atIndex:idx];
}];
}
- (void)setUpShadowViewHierarchy
{
[self updateShadowViewWithReactTag:@1 layoutOnly:NO childTags:@[@2, @3, @4, @5]];
[self updateShadowViewWithReactTag:@2 layoutOnly:YES childTags:@[@6, @7]];
[self updateShadowViewWithReactTag:@3 layoutOnly:NO childTags:nil];
[self updateShadowViewWithReactTag:@4 layoutOnly:YES childTags:@[@8]];
[self updateShadowViewWithReactTag:@5 layoutOnly:NO childTags:@[@9, @10]];
[self updateShadowViewWithReactTag:@6 layoutOnly:NO childTags:nil];
[self updateShadowViewWithReactTag:@7 layoutOnly:NO childTags:nil];
[self updateShadowViewWithReactTag:@8 layoutOnly:NO childTags:nil];
[self updateShadowViewWithReactTag:@9 layoutOnly:NO childTags:nil];
[self updateShadowViewWithReactTag:@10 layoutOnly:YES childTags:nil];
[self updateShadowViewWithReactTag:@11 layoutOnly:YES childTags:@[@12, @13]];
[self updateShadowViewWithReactTag:@12 layoutOnly:NO childTags:nil];
[self updateShadowViewWithReactTag:@13 layoutOnly:NO childTags:nil];
}
- (void)testModifyIndices1
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[@2] mutableCopy];
NSMutableArray *addIndices = [@[@3] mutableCopy];
NSMutableArray *removeIndices = [@[@0] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[@6, @7]));
XCTAssertEqualObjects(addIndices, (@[@3, @4]));
XCTAssertEqualObjects(removeIndices, (@[@0, @1]));
}
- (void)testModifyIndices2
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[@11] mutableCopy];
NSMutableArray *addIndices = [@[@4] mutableCopy];
NSMutableArray *removeIndices = [@[] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[@12, @13]));
XCTAssertEqualObjects(addIndices, (@[@5, @6]));
XCTAssertEqualObjects(removeIndices, (@[]));
}
- (void)testModifyIndices3
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[] mutableCopy];
NSMutableArray *addIndices = [@[] mutableCopy];
NSMutableArray *removeIndices = [@[@2] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[]));
XCTAssertEqualObjects(addIndices, (@[]));
XCTAssertEqualObjects(removeIndices, (@[@3]));
}
- (void)testModifyIndices4
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[@11] mutableCopy];
NSMutableArray *addIndices = [@[@3] mutableCopy];
NSMutableArray *removeIndices = [@[@2] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[@12, @13]));
XCTAssertEqualObjects(addIndices, (@[@4, @5]));
XCTAssertEqualObjects(removeIndices, (@[@3]));
}
- (void)testModifyIndices5
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[] mutableCopy];
NSMutableArray *addIndices = [@[] mutableCopy];
NSMutableArray *removeIndices = [@[@0, @1, @2, @3] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[]));
XCTAssertEqualObjects(addIndices, (@[]));
XCTAssertEqualObjects(removeIndices, (@[@0, @1, @2, @3, @4]));
}
- (void)testModifyIndices6
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[@11] mutableCopy];
NSMutableArray *addIndices = [@[@0] mutableCopy];
NSMutableArray *removeIndices = [@[@0, @1, @2, @3] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[@12, @13]));
XCTAssertEqualObjects(addIndices, (@[@0, @1]));
XCTAssertEqualObjects(removeIndices, (@[@0, @1, @2, @3, @4]));
}
- (void)testModifyIndices7
{
[self setUpShadowViewHierarchy];
NSMutableArray *addTags = [@[@11] mutableCopy];
NSMutableArray *addIndices = [@[@1] mutableCopy];
NSMutableArray *removeIndices = [@[@0, @2, @3] mutableCopy];
[self.uiManager modifyManageChildren:@1
addChildReactTags:addTags
addAtIndices:addIndices
removeAtIndices:removeIndices];
XCTAssertEqualObjects(addTags, (@[@12, @13]));
XCTAssertEqualObjects(addIndices, (@[@1, @2]));
XCTAssertEqualObjects(removeIndices, (@[@0, @1, @3, @4]));
}
- (void)testScenario1
{
RCTUIManager *uiManager = [[RCTUIManager alloc] init];
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[uiManager]; } launchOptions:nil];
NS_VALID_UNTIL_END_OF_SCOPE RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Test"];
XCTestExpectation *expectation = [self expectationWithDescription:@""];
dispatch_queue_t shadowQueue = [uiManager valueForKey:@"shadowQueue"];
dispatch_async(shadowQueue, ^{
// Make sure root view finishes loading.
dispatch_sync(dispatch_get_main_queue(), ^{});
/* */[uiManager createView:@2 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
/* */[uiManager createView:@3 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
/* V */[uiManager createView:@4 viewName:@"RCTView" rootTag:@1 props:@{@"alignItems":@"center",@"backgroundColor":@"#F5FCFF",@"flex":@1,@"justifyContent":@"center"}];
/* V */[uiManager createView:@5 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"blue",@"height":@50,@"width":@50}];
/* */[uiManager createView:@6 viewName:@"RCTView" rootTag:@1 props:@{@"width":@250}];
/* V */[uiManager createView:@7 viewName:@"RCTView" rootTag:@1 props:@{@"borderWidth":@10,@"margin":@50}];
/* V */[uiManager createView:@8 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"yellow",@"height":@50}];
/* V */[uiManager createView:@9 viewName:@"RCTText" rootTag:@1 props:@{@"accessible":@1,@"fontSize":@20,@"isHighlighted":@0,@"margin":@10,@"textAlign":@"center"}];
/* */[uiManager createView:@10 viewName:@"RCTRawText" rootTag:@1 props:@{@"text":@"This tests removal of layout-only views."}];
/* */[uiManager manageChildren:@9 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@10] addAtIndices:@[@0] removeAtIndices:nil];
/* V */[uiManager createView:@12 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"green",@"height":@50}];
/* */[uiManager manageChildren:@7 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@8,@9,@12] addAtIndices:@[@0,@1,@2] removeAtIndices:nil];
/* */[uiManager manageChildren:@6 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@7] addAtIndices:@[@0] removeAtIndices:nil];
/* V */[uiManager createView:@13 viewName:@"RCTView" rootTag:@1 props:@{@"backgroundColor":@"red",@"height":@50,@"width":@50}];
/* */[uiManager manageChildren:@4 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@5,@6,@13] addAtIndices:@[@0,@1,@2] removeAtIndices:nil];
/* */[uiManager manageChildren:@3 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@4] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@2 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@3] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@1 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@2] addAtIndices:@[@0] removeAtIndices:nil];
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)12);
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)8);
[expectation fulfill];
}];
[uiManager flushUIBlocks];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
expectation = [self expectationWithDescription:@""];
dispatch_async(shadowQueue, ^{
[uiManager updateView:@7 viewName:@"RCTView" props:@{@"borderWidth":[NSNull null]}];
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)12);
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)7);
[expectation fulfill];
}];
[uiManager flushUIBlocks];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
expectation = [self expectationWithDescription:@""];
dispatch_async(shadowQueue, ^{
[uiManager updateView:@7 viewName:@"RCTView" props:@{@"borderWidth":@10}];
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)12);
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)8);
[expectation fulfill];
}];
[uiManager flushUIBlocks];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}
- (void)testScenario2
{
RCTUIManager *uiManager = [[RCTUIManager alloc] init];
RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[uiManager]; } launchOptions:nil];
NS_VALID_UNTIL_END_OF_SCOPE RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Test"];
XCTestExpectation *expectation = [self expectationWithDescription:@""];
dispatch_queue_t shadowQueue = [uiManager valueForKey:@"shadowQueue"];
dispatch_async(shadowQueue, ^{
// Make sure root view finishes loading.
dispatch_sync(dispatch_get_main_queue(), ^{});
/* */[uiManager createView:@2 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
/* */[uiManager createView:@3 viewName:@"RCTView" rootTag:@1 props:@{@"bottom":@0,@"left":@0,@"position":@"absolute",@"right":@0,@"top":@0}];
/* V */[uiManager createView:@4 viewName:@"RCTView" rootTag:@1 props:@{@"alignItems":@"center",@"backgroundColor":@"#F5FCFF",@"flex":@1,@"justifyContent":@"center"}];
/* */[uiManager createView:@5 viewName:@"RCTView" rootTag:@1 props:@{@"width":@250}];
/* V */[uiManager createView:@6 viewName:@"RCTView" rootTag:@1 props:@{@"borderWidth":@1}];
/* V */[uiManager createView:@7 viewName:@"RCTText" rootTag:@1 props:@{@"accessible":@1,@"fontSize":@20,@"isHighlighted":@0,@"margin":@10,@"textAlign":@"center"}];
/* */[uiManager createView:@8 viewName:@"RCTRawText" rootTag:@1 props:@{@"text":@"This tests removal of layout-only views."}];
/* */[uiManager manageChildren:@7 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@8] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@6 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@7] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@5 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@6] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@4 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@5] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@3 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@4] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@2 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@3] addAtIndices:@[@0] removeAtIndices:nil];
/* */[uiManager manageChildren:@1 moveFromIndices:nil moveToIndices:nil addChildReactTags:@[@2] addAtIndices:@[@0] removeAtIndices:nil];
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)8);
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)4);
[expectation fulfill];
}];
[uiManager flushUIBlocks];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
expectation = [self expectationWithDescription:@""];
dispatch_async(shadowQueue, ^{
[uiManager updateView:@6 viewName:@"RCTView" props:@{@"borderWidth":[NSNull null]}];
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)8);
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)3);
[expectation fulfill];
}];
[uiManager flushUIBlocks];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
expectation = [self expectationWithDescription:@""];
dispatch_async(shadowQueue, ^{
[uiManager updateView:@6 viewName:@"RCTView" props:@{@"borderWidth":@1}];
[uiManager addUIBlock:^(RCTUIManager *uiManager_, __unused RCTSparseArray *viewRegistry) {
XCTAssertEqual(uiManager_.shadowViewRegistry.count, (NSUInteger)8);
XCTAssertEqual(uiManager_.viewRegistry.count, (NSUInteger)4);
[expectation fulfill];
}];
[uiManager flushUIBlocks];
});
[self waitForExpectationsWithTimeout:1 handler:nil];
}
@end

View File

@ -279,6 +279,7 @@ var ScrollView = React.createClass({
var contentContainer =
<View
collapsible={false}
ref={INNERVIEW}
style={contentContainerStyle}
removeClippedSubviews={this.props.removeClippedSubviews}>

View File

@ -77,6 +77,12 @@ var View = React.createClass({
},
propTypes: {
/**
* When false, indicates that the view should not be collapsed, even if it is
* layout-only. Defaults to true.
*/
collapsible: PropTypes.bool,
/**
* When true, indicates that the view is an accessibility element. By default,
* all the touchable elements are accessible.

View File

@ -24,6 +24,18 @@ ReactNativeViewAttributes.UIView = {
onLayout: true,
onAccessibilityTap: true,
onMagicTap: true,
collapsible: true,
// If any below are set, view should not be collapsible!
onMoveShouldSetResponder: true,
onResponderGrant: true,
onResponderMove: true,
onResponderReject: true,
onResponderRelease: true,
onResponderTerminate: true,
onResponderTerminationRequest: true,
onStartShouldSetResponder: true,
onStartShouldSetResponderCapture: true,
};
ReactNativeViewAttributes.RCTView = merge(

View File

@ -20,6 +20,11 @@
}
}
- (BOOL)isLayoutOnly
{
return YES;
}
- (NSString *)description
{
NSString *superDescription = super.description;

View File

@ -33,15 +33,8 @@
#import "RCTViewNodeProtocol.h"
#import "UIView+React.h"
typedef void (^react_view_node_block_t)(id<RCTViewNodeProtocol>);
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, react_view_node_block_t block)
{
if (view.reactTag) block(view);
for (id<RCTViewNodeProtocol> subview in view.reactSubviews) {
RCTTraverseViewNodes(subview, block);
}
}
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, void (^block)(id<RCTViewNodeProtocol>));
static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps);
@interface RCTAnimation : NSObject
@ -467,6 +460,24 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
[rootShadowView collectRootUpdatedFrames:viewsWithNewFrames
parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}];
NSSet *originalViewsWithNewFrames = [viewsWithNewFrames copy];
NSMutableArray *viewsToCheck = [viewsWithNewFrames.allObjects mutableCopy];
while (viewsToCheck.count > 0) {
// Better to remove from the front and append to the end
// because of how NSMutableArray is implemented.
// (It's a "round" buffer with stored size and offset.)
RCTShadowView *viewToCheck = viewsToCheck.firstObject;
[viewsToCheck removeObjectAtIndex:0];
if (viewToCheck.layoutOnly) {
[viewsWithNewFrames removeObject:viewToCheck];
[viewsToCheck addObjectsFromArray:[viewToCheck reactSubviews]];
} else {
[viewsWithNewFrames addObject:viewToCheck];
}
}
// Parallel arrays are built and then handed off to main thread
NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
@ -475,26 +486,30 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count];
for (RCTShadowView *shadowView in viewsWithNewFrames) {
[frameReactTags addObject:shadowView.reactTag];
[frames addObject:[NSValue valueWithCGRect:shadowView.frame]];
CGRect frame = shadowView.adjustedFrame;
NSNumber *reactTag = shadowView.reactTag;
[frameReactTags addObject:reactTag];
[frames addObject:[NSValue valueWithCGRect:frame]];
[areNew addObject:@(shadowView.isNewView)];
[parentsAreNew addObject:@(shadowView.superview.isNewView)];
id event = (id)kCFNull;
if (shadowView.hasOnLayout) {
event = @{
@"target": shadowView.reactTag,
@"layout": @{
@"x": @(shadowView.frame.origin.x),
@"y": @(shadowView.frame.origin.y),
@"width": @(shadowView.frame.size.width),
@"height": @(shadowView.frame.size.height),
},
};
RCTShadowView *superview = shadowView;
BOOL parentIsNew = NO;
while (YES) {
superview = superview.superview;
parentIsNew = superview.isNewView;
if (!superview.layoutOnly) {
break;
}
}
[parentsAreNew addObject:@(parentIsNew)];
id event = shadowView.hasOnLayout
? RCTShadowViewOnLayoutEventPayload(shadowView.reactTag, frame)
: (id)kCFNull;
[onLayoutEvents addObject:event];
}
for (RCTShadowView *shadowView in viewsWithNewFrames) {
for (RCTShadowView *shadowView in originalViewsWithNewFrames) {
// We have to do this after we build the parentsAreNew array.
shadowView.newView = NO;
}
@ -511,24 +526,28 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
}
// Perform layout (possibly animated)
return ^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
RCTResponseSenderBlock callback = self->_layoutAnimation.callback;
return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
RCTResponseSenderBlock callback = uiManager->_layoutAnimation.callback;
__block NSUInteger completionsCalled = 0;
for (NSUInteger ii = 0; ii < frames.count; ii++) {
NSNumber *reactTag = frameReactTags[ii];
UIView *view = viewRegistry[reactTag];
if (!view) {
continue;
}
CGRect frame = [frames[ii] CGRectValue];
id event = onLayoutEvents[ii];
BOOL isNew = [areNew[ii] boolValue];
RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation;
RCTAnimation *updateAnimation = isNew ? nil : uiManager->_layoutAnimation.updateAnimation;
BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue];
RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil;
RCTAnimation *createAnimation = shouldAnimateCreation ? uiManager->_layoutAnimation.createAnimation : nil;
void (^completion)(BOOL) = ^(BOOL finished) {
completionsCalled++;
if (event != (id)kCFNull) {
[self.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event];
[uiManager.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event];
}
if (callback && completionsCalled == frames.count - 1) {
callback(@[@(finished)]);
@ -540,13 +559,13 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
[updateAnimation performAnimations:^{
[view reactSetFrame:frame];
for (RCTViewManagerUIBlock block in updateBlocks) {
block(self, _viewRegistry);
block(uiManager, viewRegistry);
}
} withCompletionBlock:completion];
} else {
[view reactSetFrame:frame];
for (RCTViewManagerUIBlock block in updateBlocks) {
block(self, _viewRegistry);
block(uiManager, viewRegistry);
}
completion(YES);
}
@ -568,7 +587,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
createAnimation.property);
}
for (RCTViewManagerUIBlock block in updateBlocks) {
block(self, _viewRegistry);
block(uiManager, viewRegistry);
}
} withCompletionBlock:nil];
}
@ -691,6 +710,135 @@ RCT_EXPORT_METHOD(replaceExistingNonRootView:(NSNumber *)reactTag withView:(NSNu
removeAtIndices:removeAtIndices];
}
/**
* This method modifies the indices received in manageChildren() to take into
* account views that are layout only. For example, if JS tells native to insert
* view with tag 12 at index 4, but view 12 is layout only, we would want to
* insert its children's tags, tags 13 and 14, at indices 4 and 5 instead. This
* causes us to have to shift the remaining indices to account for the new
* views.
*/
- (void)modifyManageChildren:(NSNumber *)containerReactTag
addChildReactTags:(NSMutableArray *)mutableAddChildReactTags
addAtIndices:(NSMutableArray *)mutableAddAtIndices
removeAtIndices:(NSMutableArray *)mutableRemoveAtIndices
{
NSUInteger i;
NSMutableArray *containerSubviews = [[_shadowViewRegistry[containerReactTag] reactSubviews] mutableCopy];
i = 0;
while (i < containerSubviews.count) {
RCTShadowView *shadowView = containerSubviews[i];
if (!shadowView.layoutOnly) {
i++;
continue;
}
[containerSubviews removeObjectAtIndex:i];
NSArray *subviews = [shadowView reactSubviews];
NSUInteger subviewsCount = subviews.count;
NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)];
[containerSubviews insertObjects:subviews atIndexes:insertionIndexes];
NSUInteger removalIndex = [mutableRemoveAtIndices indexOfObject:@(i)];
if (removalIndex != NSNotFound) {
[mutableRemoveAtIndices removeObjectAtIndex:removalIndex];
}
if (subviewsCount != 1) {
for (NSUInteger j = 0, count = mutableRemoveAtIndices.count; j < count; j++) {
NSUInteger atIndex = [mutableRemoveAtIndices[j] unsignedIntegerValue];
if (atIndex > i) {
mutableRemoveAtIndices[j] = @(atIndex + subviewsCount - 1);
}
}
}
if (removalIndex != NSNotFound) {
for (NSUInteger j = 0; j < subviewsCount; j++) {
[mutableRemoveAtIndices insertObject:@(i + j) atIndex:removalIndex + j];
}
}
if (removalIndex == NSNotFound && subviewsCount != 1) {
for (NSUInteger j = 0, count = mutableAddAtIndices.count; j < count; j++) {
NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue];
if (atIndex > i) {
mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1);
}
}
}
}
i = 0;
while (i < mutableAddChildReactTags.count) {
NSNumber *tag = mutableAddChildReactTags[i];
NSNumber *index = mutableAddAtIndices[i];
RCTShadowView *shadowView = _shadowViewRegistry[tag];
if (!shadowView.layoutOnly) {
i++;
continue;
}
NSArray *subviews = [shadowView reactSubviews];
NSUInteger subviewsCount = subviews.count;
[mutableAddAtIndices removeObjectAtIndex:i];
[mutableAddChildReactTags removeObjectAtIndex:i];
for (NSUInteger j = 0; j < subviewsCount; j++) {
[mutableAddChildReactTags insertObject:[subviews[j] reactTag] atIndex:i + j];
[mutableAddAtIndices insertObject:@(index.unsignedIntegerValue + j) atIndex:i + j];
}
for (NSUInteger j = i + subviewsCount, count = mutableAddAtIndices.count; j < count; j++) {
NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue];
mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1);
}
}
}
- (NSNumber *)containerReactTag:(NSNumber *)containerReactTag offset:(inout NSUInteger *)offset
{
RCTShadowView *container = _shadowViewRegistry[containerReactTag];
NSNumber *containerSuperviewReactTag = containerReactTag;
RCTShadowView *superview = container;
while (superview.layoutOnly) {
RCTShadowView *superviewSuperview = superview.superview;
containerSuperviewReactTag = superviewSuperview.reactTag;
NSMutableArray *reactSubviews = [[superviewSuperview reactSubviews] mutableCopy];
NSUInteger superviewIndex = [reactSubviews indexOfObject:superview];
NSUInteger i = 0;
while (i < superviewIndex) {
RCTShadowView *child = reactSubviews[i];
if (!child.layoutOnly) {
if (offset) {
(*offset)++;
}
i++;
continue;
}
[reactSubviews removeObjectAtIndex:i];
NSArray *subviews = [child reactSubviews];
NSUInteger subviewsCount = subviews.count;
NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)];
[reactSubviews insertObjects:subviews atIndexes:insertionIndexes];
superviewIndex += subviewsCount - 1;
}
superview = superviewSuperview;
}
return containerSuperviewReactTag;
}
RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
moveFromIndices:(NSArray *)moveFromIndices
moveToIndices:(NSArray *)moveToIndices
@ -698,62 +846,109 @@ RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
addAtIndices:(NSArray *)addAtIndices
removeAtIndices:(NSArray *)removeAtIndices)
{
RCTShadowView *container = _shadowViewRegistry[containerReactTag];
NSUInteger offset = 0;
NSNumber *containerSuperviewReactTag = [self containerReactTag:containerReactTag offset:&offset];
RCTAssert(moveFromIndices.count == moveToIndices.count, @"Invalid argument: moveFromIndices.count != moveToIndices.count");
if (moveFromIndices.count > 0) {
NSMutableArray *mutableAddChildReactTags = [addChildReactTags mutableCopy];
NSMutableArray *mutableAddAtIndices = [addAtIndices mutableCopy];
NSMutableArray *mutableRemoveAtIndices = [removeAtIndices mutableCopy];
NSArray *containerSubviews = [container reactSubviews];
for (NSUInteger i = 0, count = moveFromIndices.count; i < count; i++) {
NSNumber *from = moveFromIndices[i];
NSNumber *to = moveToIndices[i];
[mutableAddChildReactTags addObject:[containerSubviews[from.unsignedIntegerValue] reactTag]];
[mutableAddAtIndices addObject:to];
[mutableRemoveAtIndices addObject:from];
}
addChildReactTags = mutableAddChildReactTags;
addAtIndices = mutableAddAtIndices;
removeAtIndices = mutableRemoveAtIndices;
}
NSMutableArray *mutableAddChildReactTags;
NSMutableArray *mutableAddAtIndices;
NSMutableArray *mutableRemoveAtIndices;
if (containerSuperviewReactTag) {
mutableAddChildReactTags = [addChildReactTags mutableCopy];
mutableAddAtIndices = [addAtIndices mutableCopy];
mutableRemoveAtIndices = [removeAtIndices mutableCopy];
[self modifyManageChildren:containerReactTag
addChildReactTags:mutableAddChildReactTags
addAtIndices:mutableAddAtIndices
removeAtIndices:mutableRemoveAtIndices];
if (offset > 0) {
NSUInteger count = MAX(mutableAddAtIndices.count, mutableRemoveAtIndices.count);
for (NSUInteger i = 0; i < count; i++) {
if (i < mutableAddAtIndices.count) {
NSUInteger index = [mutableAddAtIndices[i] unsignedIntegerValue];
mutableAddAtIndices[i] = @(index + offset);
}
if (i < mutableRemoveAtIndices.count) {
NSUInteger index = [mutableRemoveAtIndices[i] unsignedIntegerValue];
mutableRemoveAtIndices[i] = @(index + offset);
}
}
}
}
[self _manageChildren:containerReactTag
moveFromIndices:moveFromIndices
moveToIndices:moveToIndices
addChildReactTags:addChildReactTags
addAtIndices:addAtIndices
removeAtIndices:removeAtIndices
registry:_shadowViewRegistry];
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
[uiManager _manageChildren:containerReactTag
moveFromIndices:moveFromIndices
moveToIndices:moveToIndices
addChildReactTags:addChildReactTags
addAtIndices:addAtIndices
removeAtIndices:removeAtIndices
registry:viewRegistry];
}];
if (containerSuperviewReactTag) {
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
(void)(id []){containerReactTag, @(offset), addChildReactTags, addAtIndices, removeAtIndices};
[uiManager _manageChildren:containerSuperviewReactTag
addChildReactTags:mutableAddChildReactTags
addAtIndices:mutableAddAtIndices
removeAtIndices:mutableRemoveAtIndices
registry:viewRegistry];
}];
}
}
- (void)_manageChildren:(NSNumber *)containerReactTag
moveFromIndices:(NSArray *)moveFromIndices
moveToIndices:(NSArray *)moveToIndices
addChildReactTags:(NSArray *)addChildReactTags
addAtIndices:(NSArray *)addAtIndices
removeAtIndices:(NSArray *)removeAtIndices
registry:(RCTSparseArray *)registry
{
id<RCTViewNodeProtocol> container = registry[containerReactTag];
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");
RCTAssert(addChildReactTags.count == addAtIndices.count, @"Invalid arguments: addChildReactTags.count == addAtIndices.count");
// Removes (both permanent and temporary moves) are using "before" indices
NSArray *permanentlyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
NSArray *temporarilyRemovedChildren = [self _childrenToRemoveFromContainer:container atIndices:moveFromIndices];
[self _removeChildren:permanentlyRemovedChildren fromContainer:container];
[self _removeChildren:temporarilyRemovedChildren fromContainer:container];
// Removes are using "before" indices
NSArray *removedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices];
[self _removeChildren:removedChildren fromContainer:container];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT reactTag in %@", addChildReactTags];
NSArray *permanentlyRemovedChildren = [removedChildren filteredArrayUsingPredicate:predicate];
[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
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]];
// Figure out what to insert
NSMutableDictionary *childrenToAdd = [NSMutableDictionary dictionary];
for (NSInteger index = 0, count = addAtIndices.count; index < count; index++) {
id<RCTViewNodeProtocol> view = registry[addChildReactTags[index]];
if (view) {
destinationsToChildrenToAdd[addAtIndices[index]] = view;
childrenToAdd[addAtIndices[index]] = view;
}
}
NSArray *sortedIndices = [[destinationsToChildrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)];
NSArray *sortedIndices = [[childrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)];
for (NSNumber *reactIndex in sortedIndices) {
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue];
[container insertReactSubview:childrenToAdd[reactIndex] atIndex:reactIndex.integerValue];
}
}
@ -836,45 +1031,72 @@ RCT_EXPORT_METHOD(createView:(NSNumber *)reactTag
// Set properties
shadowView.viewName = viewName;
shadowView.reactTag = reactTag;
shadowView.allProps = props;
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], manager);
}
_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;
if (!shadowView.layoutOnly) {
// 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;
[self addUIBlock:^(RCTUIManager *uiManager, __unused RCTSparseArray *viewRegistry) {
[uiManager createView:reactTag viewName:viewName props:props withManager:manager backgroundColor:backgroundColor];
}];
}
}
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){
RCTAssertMainThread();
- (UIView *)createView:(NSNumber *)reactTag viewName:(NSString *)viewName props:(NSDictionary *)props withManager:(RCTViewManager *)manager backgroundColor:(UIColor *)backgroundColor
{
RCTAssertMainThread();
UIView *view = [manager view];
if (!view) {
return nil;
}
UIView *view = [manager view];
if (view) {
// Generate default view, used for resetting default props
if (!_defaultViews[viewName]) {
// Note the default is setup after the props are read for the first time
// ever for this className - this is ok because we only use the default
// for restoring defaults, which never happens on first creation.
_defaultViews[viewName] = [manager view];
}
// Generate default view, used for resetting default props
if (!uiManager->_defaultViews[viewName]) {
// Note the default is setup after the props are read for the first time
// ever for this className - this is ok because we only use the default
// for restoring defaults, which never happens on first creation.
uiManager->_defaultViews[viewName] = [manager view];
}
// Set properties
view.reactTag = reactTag;
view.backgroundColor = backgroundColor;
if ([view isKindOfClass:[UIView class]]) {
view.multipleTouchEnabled = YES;
view.userInteractionEnabled = YES; // required for touch handling
view.layer.allowsGroupOpacity = YES; // required for touch handling
}
RCTSetViewProps(props, view, _defaultViews[viewName], manager);
// Set properties
view.reactTag = reactTag;
view.backgroundColor = backgroundColor;
if ([view isKindOfClass:[UIView class]]) {
view.multipleTouchEnabled = YES;
view.userInteractionEnabled = YES; // required for touch handling
view.layer.allowsGroupOpacity = YES; // required for touch handling
}
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], manager);
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
[_bridgeTransactionListeners addObject:view];
}
_viewRegistry[reactTag] = view;
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
[uiManager->_bridgeTransactionListeners addObject:view];
}
}
viewRegistry[reactTag] = view;
}];
return view;
}
NS_INLINE BOOL RCTRectIsDefined(CGRect frame)
{
return !(isnan(frame.origin.x) || isnan(frame.origin.y) || isnan(frame.size.width) || isnan(frame.size.height));
}
NS_INLINE NSDictionary *RCTShadowViewOnLayoutEventPayload(NSNumber *reactTag, CGRect frame)
{
return @{
@"target": reactTag,
@"layout": @{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(frame.size.width),
@"height": @(frame.size.height),
},
};
}
// TODO: remove viewName param as it isn't needed
@ -888,10 +1110,100 @@ RCT_EXPORT_METHOD(updateView:(NSNumber *)reactTag
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], viewManager);
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag];
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager);
}];
const BOOL wasLayoutOnly = shadowView.layoutOnly;
NSDictionary *newProps = RCTPropsMerge(shadowView.allProps, props);
shadowView.allProps = newProps;
const BOOL isLayoutOnly = shadowView.layoutOnly;
if (wasLayoutOnly != isLayoutOnly) {
// Add/remove node
if (isLayoutOnly) {
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
RCTAssertMainThread();
UIView *container = viewRegistry[reactTag];
const CGRect containerFrame = container.frame;
const CGFloat deltaX = containerFrame.origin.x;
const CGFloat deltaY = containerFrame.origin.y;
NSUInteger offset = [container.superview.subviews indexOfObject:container];
[container.subviews enumerateObjectsUsingBlock:^(UIView *subview, NSUInteger idx, __unused BOOL *stop) {
[container removeReactSubview:subview];
CGRect subviewFrame = subview.frame;
subviewFrame.origin.x += deltaX;
subviewFrame.origin.y += deltaY;
subview.frame = subviewFrame;
[container.superview insertReactSubview:subview atIndex:idx + offset];
}];
[container.superview removeReactSubview:container];
if ([container conformsToProtocol:@protocol(RCTInvalidating)]) {
[(id<RCTInvalidating>)container invalidate];
}
viewRegistry[reactTag] = nil;
}];
} else {
NSMutableArray *mutableAddChildReactTags = [[[shadowView reactSubviews] valueForKey:@"reactTag"] mutableCopy];
NSMutableArray *mutableAddAtIndices = [NSMutableArray arrayWithCapacity:mutableAddChildReactTags.count];
for (NSUInteger i = 0, count = mutableAddChildReactTags.count; i < count; i++) {
[mutableAddAtIndices addObject:@(i)];
}
[self modifyManageChildren:reactTag
addChildReactTags:mutableAddChildReactTags
addAtIndices:mutableAddAtIndices
removeAtIndices:nil];
NSUInteger offset = [shadowView.superview.reactSubviews indexOfObject:shadowView];
NSNumber *containerSuperviewReactTag = [self containerReactTag:shadowView.superview.reactTag offset:&offset];
UIColor *backgroundColor = shadowView.backgroundColor;
CGRect shadowViewFrame = shadowView.adjustedFrame;
NSMutableDictionary *newFrames = [NSMutableDictionary dictionaryWithCapacity:mutableAddChildReactTags.count];
for (NSNumber *childTag in mutableAddChildReactTags) {
RCTShadowView *child = _shadowViewRegistry[childTag];
newFrames[childTag] = [NSValue valueWithCGRect:child.adjustedFrame];
}
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
RCTAssertMainThread();
UIView *containerSuperview = viewRegistry[containerSuperviewReactTag];
UIView *container = [uiManager createView:reactTag viewName:viewName props:newProps withManager:viewManager backgroundColor:backgroundColor];
[containerSuperview insertReactSubview:container atIndex:offset];
if (RCTRectIsDefined(shadowViewFrame)) {
container.frame = shadowViewFrame;
}
for (NSUInteger i = 0, count = mutableAddAtIndices.count; i < count; i++) {
NSNumber *tag = mutableAddChildReactTags[i];
UIView *subview = viewRegistry[tag];
[containerSuperview removeReactSubview:subview];
NSUInteger atIndex = [mutableAddAtIndices[i] unsignedIntegerValue];
[container insertReactSubview:subview atIndex:atIndex];
CGRect subviewFrame = [newFrames[tag] CGRectValue];
if (RCTRectIsDefined(subviewFrame)) {
subview.frame = subviewFrame;
}
}
}];
}
} else if (!isLayoutOnly) {
// Update node
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
UIView *view = viewRegistry[reactTag];
RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager);
}];
}
}
RCT_EXPORT_METHOD(focus:(NSNumber *)reactTag)
@ -1227,12 +1539,16 @@ RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag
RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag
blockNativeResponder:(__unused BOOL)blockNativeResponder)
{
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
_jsResponder = viewRegistry[reactTag];
if (!_jsResponder) {
RCTLogError(@"Invalid view set to be the JS responder - tag %zd", reactTag);
}
}];
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
if (!shadowView) {
RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag);
} else if (shadowView.layoutOnly) {
RCTLogError(@"Cannot set JS responder to layout-only view with tag %@ - %@, %@", reactTag, shadowView, shadowView.allProps);
} else {
[self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
_jsResponder = viewRegistry[reactTag];
}];
}
}
RCT_EXPORT_METHOD(clearJSResponder)
@ -1488,3 +1804,27 @@ static UIView *_jsResponder;
}
@end
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, void (^block)(id<RCTViewNodeProtocol>))
{
if (view.reactTag) block(view);
for (id<RCTViewNodeProtocol> subview in view.reactSubviews) {
RCTTraverseViewNodes(subview, block);
}
}
static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps)
{
NSMutableDictionary *afterProps = [NSMutableDictionary dictionaryWithDictionary:beforeProps];
// Can't use -addEntriesFromDictionary: because we want to remove keys with NSNull values.
[newProps enumerateKeysAndObjectsUsingBlock:^(id key, id obj, __unused BOOL *stop) {
if (obj == (id)kCFNull) {
[afterProps removeObjectForKey:key];
} else {
afterProps[key] = obj;
}
}];
return afterProps;
}

View File

@ -41,6 +41,12 @@ typedef void (^RCTApplierBlock)(RCTSparseArray *viewRegistry);
@property (nonatomic, assign) RCTUpdateLifecycle layoutLifecycle;
@property (nonatomic, assign) BOOL hasOnLayout;
@property (nonatomic, assign, readonly, getter=isLayoutOnly) BOOL layoutOnly;
@property (nonatomic, copy) NSDictionary *allProps;
/// `frame` adjusted for recursive superview `layoutOnly` status.
@property (nonatomic, assign, readonly) CGRect adjustedFrame;
/**
* isNewView - Used to track the first time the view is introduced into the hierarchy. It is initialized YES, then is
* set to NO in RCTUIManager after the layout pass is done and all frames have been extracted to be applied to the

View File

@ -367,8 +367,10 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
- (NSString *)description
{
NSString *description = super.description;
description = [[description substringToIndex:description.length - 1] stringByAppendingFormat:@"; viewName: %@; reactTag: %@; frame: %@>", self.viewName, self.reactTag, NSStringFromCGRect(self.frame)];
return description;
if (self.layoutOnly) {
description = [@"* " stringByAppendingString:description];
}
return [[description substringToIndex:description.length - 1] stringByAppendingFormat:@"; viewName: %@; reactTag: %@; frame: %@>", self.viewName, self.reactTag, NSStringFromCGRect(self.frame)];
}
- (void)addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level
@ -392,6 +394,91 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
return description;
}
- (BOOL)isLayoutOnly
{
if (![self.viewName isEqualToString:@"RCTView"]) {
// For now, only `RCTView`s can be layout-only.
return NO;
}
// dispatch_once is unnecessary because this property SHOULD only be accessed
// on the shadow queue
static NSSet *layoutKeys;
static NSSet *specialKeys;
if (!layoutKeys || !specialKeys) {
// Taken from LayoutPropTypes.js with the exception that borderWidth,
// borderTopWidth, borderBottomWidth, borderLeftWidth, and borderRightWidth
// were removed because black color is assumed
static NSString *const layoutKeyStrings[] = {
@"width",
@"height",
@"top",
@"left",
@"right",
@"bottom",
@"margin",
@"marginVertical",
@"marginHorizontal",
@"marginTop",
@"marginBottom",
@"marginLeft",
@"marginRight",
@"padding",
@"paddingVertical",
@"paddingHorizontal",
@"paddingTop",
@"paddingBottom",
@"paddingLeft",
@"paddingRight",
@"position",
@"flexDirection",
@"flexWrap",
@"justifyContent",
@"alignItems",
@"alignSelf",
@"flex",
};
layoutKeys = [NSSet setWithObjects:layoutKeyStrings count:sizeof(layoutKeyStrings)/sizeof(*layoutKeyStrings)];
static NSString *const specialKeyStrings[] = {
@"accessible",
@"collapsible",
};
specialKeys = [NSSet setWithObjects:specialKeyStrings count:sizeof(specialKeyStrings)/sizeof(*specialKeyStrings)];
}
NSNumber *collapsible = self.allProps[@"collapsible"];
if (collapsible && !collapsible.boolValue) {
return NO;
}
NSNumber *accessible = self.allProps[@"accessible"];
if (accessible && accessible.boolValue) {
return NO;
}
for (NSString *key in self.allProps) {
if (![specialKeys containsObject:key] && ![layoutKeys containsObject:key]) {
return NO;
}
}
return YES;
}
- (CGRect)adjustedFrame
{
CGRect frame = self.frame;
RCTShadowView *superview = self;
while ((superview = superview.superview) && superview.layoutOnly) {
const CGRect superviewFrame = superview.frame;
frame.origin.x += superviewFrame.origin.x;
frame.origin.y += superviewFrame.origin.y;
}
return frame;
}
// Margin
#define RCT_MARGIN_PROPERTY(prop, metaProp) \

View File

@ -15,10 +15,11 @@
@protocol RCTViewNodeProtocol <NSObject>
@property (nonatomic, copy) NSNumber *reactTag;
@property (nonatomic, assign) CGRect frame;
- (void)insertReactSubview:(id<RCTViewNodeProtocol>)subview atIndex:(NSInteger)atIndex;
- (void)removeReactSubview:(id<RCTViewNodeProtocol>)subview;
- (NSMutableArray *)reactSubviews;
- (NSArray *)reactSubviews;
- (id<RCTViewNodeProtocol>)reactSuperview;
- (NSNumber *)reactTagAtPoint:(CGPoint)point;