diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExampleSnapshot_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExampleSnapshot_1@2x.png index 927c52347..b73cbc012 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExampleSnapshot_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExampleSnapshot_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index 388761a8f..0fd4a7064 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -2,6 +2,8 @@ #import +#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 diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 512d8dbd6..f01cce58a 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -279,6 +279,7 @@ var ScrollView = React.createClass({ var contentContainer = diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index c6a279a22..0cb6e4a4f 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -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. diff --git a/Libraries/ReactNative/ReactNativeViewAttributes.js b/Libraries/ReactNative/ReactNativeViewAttributes.js index 50b839e1d..eb3a396ca 100644 --- a/Libraries/ReactNative/ReactNativeViewAttributes.js +++ b/Libraries/ReactNative/ReactNativeViewAttributes.js @@ -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( diff --git a/Libraries/Text/RCTShadowRawText.m b/Libraries/Text/RCTShadowRawText.m index e99e1187b..00a3490bc 100644 --- a/Libraries/Text/RCTShadowRawText.m +++ b/Libraries/Text/RCTShadowRawText.m @@ -20,6 +20,11 @@ } } +- (BOOL)isLayoutOnly +{ + return YES; +} + - (NSString *)description { NSString *superDescription = super.description; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index ef2d367a0..3d81801cd 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -33,15 +33,8 @@ #import "RCTViewNodeProtocol.h" #import "UIView+React.h" -typedef void (^react_view_node_block_t)(id); - -static void RCTTraverseViewNodes(id view, react_view_node_block_t block) -{ - if (view.reactTag) block(view); - for (id subview in view.reactSubviews) { - RCTTraverseViewNodes(subview, block); - } -} +static void RCTTraverseViewNodes(id view, void (^block)(id)); +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 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 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)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 view, void (^block)(id)) +{ + if (view.reactTag) block(view); + for (id 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; +} diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 1c44033f6..38edc6e50 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -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 diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 9d56bb906..2c350b18b 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -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) \ diff --git a/React/Views/RCTViewNodeProtocol.h b/React/Views/RCTViewNodeProtocol.h index e78cc2ce7..96eb78f1a 100644 --- a/React/Views/RCTViewNodeProtocol.h +++ b/React/Views/RCTViewNodeProtocol.h @@ -15,10 +15,11 @@ @protocol RCTViewNodeProtocol @property (nonatomic, copy) NSNumber *reactTag; +@property (nonatomic, assign) CGRect frame; - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex; - (void)removeReactSubview:(id)subview; -- (NSMutableArray *)reactSubviews; +- (NSArray *)reactSubviews; - (id)reactSuperview; - (NSNumber *)reactTagAtPoint:(CGPoint)point;