diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index c266cabcf..ae9050662 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 14DC67F11AB71876001358AB /* libRCTPushNotification.a */; }; 3578590A1B28D2CF00341EDB /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 357859011B28D2C500341EDB /* libRCTLinking.a */; }; 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 834C36D21AF8DA610019C93C /* libRCTSettings.a */; }; + 83686E801B39D26300CBA10B /* Test.includeRequire.runModule.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 83686E7F1B39D26300CBA10B /* Test.includeRequire.runModule.bundle */; }; D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D85B829C1AB6D5CE003F4FE2 /* libRCTVibration.a */; }; /* End PBXBuildFile section */ @@ -202,6 +203,7 @@ 14E0EEC81AB118F7000DECC3 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 357858F81B28D2C400341EDB /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../../Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTTest.xcodeproj; path = ../../Libraries/RCTTest/RCTTest.xcodeproj; sourceTree = ""; }; + 83686E7F1B39D26300CBA10B /* Test.includeRequire.runModule.bundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Test.includeRequire.runModule.bundle; sourceTree = ""; }; D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -351,6 +353,7 @@ 1497CFAB1B21F5E400C1F8F2 /* RCTUIManagerTests.m */, 143BC57E1B21E18100462512 /* Info.plist */, 14D6D7101B220EB3001FB087 /* libOCMock.a */, + 83686E7F1B39D26300CBA10B /* Test.includeRequire.runModule.bundle */, 14D6D7011B220AE3001FB087 /* OCMock */, 143BC57F1B21E18100462512 /* ReferenceImages */, ); @@ -745,6 +748,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 83686E801B39D26300CBA10B /* Test.includeRequire.runModule.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 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 d37b70879..cf264cbe7 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..8e848ede4 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,331 @@ } } +/* +-----------------------------------------------------------+ +----------------------+ + * | 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]; + NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"Test.includeRequire.runModule" withExtension:nil]; + RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL 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]; + NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"Test.includeRequire.runModule" withExtension:nil]; + RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:bundleURL 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/Examples/UIExplorer/UIExplorerUnitTests/Test.includeRequire.runModule.bundle b/Examples/UIExplorer/UIExplorerUnitTests/Test.includeRequire.runModule.bundle new file mode 100644 index 000000000..519e3950e --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/Test.includeRequire.runModule.bundle @@ -0,0 +1,1120 @@ +__DEV__ = +true; +( +function(global){ + + +if(global.require){ +return;} + + +var __DEV__=global.__DEV__; + +var toString=Object.prototype.toString; + + + + + + + + + + + + + +var modulesMap={}, + + + + + + +dependencyMap={}, + + + +predefinedRefCounts={}, + +_counter=0, + +REQUIRE_WHEN_READY=1, +USED_AS_TRANSPORT=2, + +hop=Object.prototype.hasOwnProperty; + +function _debugUnresolvedDependencies(names){ +var unresolved=Array.prototype.slice.call(names); +var visited={}; +var ii, name, module, dependency; + +while(unresolved.length) { +name = unresolved.shift(); +if(visited[name]){ +continue;} + +visited[name] = true; + +module = modulesMap[name]; +if(!module || !module.waiting){ +continue;} + + +for(ii = 0; ii < module.dependencies.length; ii++) { +dependency = module.dependencies[ii]; +if(!modulesMap[dependency] || modulesMap[dependency].waiting){ +unresolved.push(dependency);}}} + + + + +for(name in visited) if(hop.call(visited, name)){ +unresolved.push(name);} + + +var messages=[]; +for(ii = 0; ii < unresolved.length; ii++) { +name = unresolved[ii]; +var message=name; +module = modulesMap[name]; +if(!module){ +message += ' is not defined';}else +if(!module.waiting){ +message += ' is ready';}else +{ +var unresolvedDependencies=[]; +for(var jj=0; jj < module.dependencies.length; jj++) { +dependency = module.dependencies[jj]; +if(!modulesMap[dependency] || modulesMap[dependency].waiting){ +unresolvedDependencies.push(dependency);}} + + +message += ' is waiting for ' + unresolvedDependencies.join(', ');} + +messages.push(message);} + +return messages.join('\n');} + + + + + +function ModuleError(msg){ +this.name = 'ModuleError'; +this.message = msg; +this.stack = Error(msg).stack; +this.framesToPop = 2;} + +ModuleError.prototype = Object.create(Error.prototype); +ModuleError.prototype.constructor = ModuleError; + +var _performance= +global.performance || +global.msPerformance || +global.webkitPerformance || {}; + +if(!_performance.now){ +_performance = global.Date;} + + +var _now=_performance? +_performance.now.bind(_performance):function(){return 0;}; + +var _factoryStackCount=0; +var _factoryTime=0; +var _totalFactories=0; +var _inGuard=false; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +function require(id){ +var module=modulesMap[id], dep, i, msg; +if(module && module.exports){ + + +if(module.refcount-- === 1){ +delete modulesMap[id];} + +return module.exports;} + +if(global.ErrorUtils && !_inGuard){ +_inGuard = true; +try{ +var ret=require.apply(this, arguments);} +catch(e) { +global.ErrorUtils.reportFatalError(e);} + +_inGuard = false; +return ret;} + + +if(!module){ +msg = 'Requiring unknown module "' + id + '"'; +if(__DEV__){ +msg += '. If you are sure the module is there, try restarting the packager.';} + +throw new ModuleError(msg);} + + +if(module.hasError){ +throw new ModuleError( +'Requiring module "' + id + '" which threw an exception');} + + + +if(module.waiting){ +throw new ModuleError( +'Requiring module "' + id + '" with unresolved dependencies: ' + +_debugUnresolvedDependencies([id]));} + + + +var exports=module.exports = {}; +var factory=module.factory; +if(toString.call(factory) === '[object Function]'){ +var args=[], +dependencies=module.dependencies, +length=dependencies.length, +ret; +if(module.special & USED_AS_TRANSPORT){ +length = Math.min(length, factory.length);} + +try{ +for(i = 0; args.length < length; i++) { +dep = dependencies[i]; +if(!module.inlineRequires[dep]){ +args.push(dep === 'module'?module: +dep === 'exports'?exports: +require.call(null, dep));}} + + + +++_totalFactories; +if(_factoryStackCount++ === 0){ +_factoryTime -= _now();} + +try{ +ret = factory.apply(module.context || global, args);} +catch(e) { +if(modulesMap.ex && modulesMap.erx){ + + +var ex=require.call(null, 'ex'); +var erx=require.call(null, 'erx'); +var messageWithParams=erx(e.message); +if(messageWithParams[0].indexOf(' from module "%s"') < 0){ +messageWithParams[0] += ' from module "%s"'; +messageWithParams[messageWithParams.length] = id;} + +e.message = ex.apply(null, messageWithParams);} + +throw e;}finally +{ +if(--_factoryStackCount === 0){ +_factoryTime += _now();}}} + + +catch(e) { +module.hasError = true; +module.exports = null; +throw e;} + +if(ret){ +if(__DEV__){ +if(typeof ret != 'object' && typeof ret != 'function'){ +throw new ModuleError( +'Factory for module "' + id + '" returned ' + +'an invalid value "' + ret + '". ' + +'Returned value should be either a function or an object.');}} + + + +module.exports = ret;}}else + +{ +module.exports = factory;} + + + + +if(module.refcount-- === 1){ +delete modulesMap[id];} + +return module.exports;} + + +require.__getFactoryTime = function(){ +return (_factoryStackCount?_now():0) + _factoryTime;}; + + +require.__getTotalFactories = function(){ +return _totalFactories;}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +function define(id, dependencies, factory, +_special, _context, _refCount, _inlineRequires){ +if(dependencies === undefined){ +dependencies = []; +factory = id; +id = _uid();}else +if(factory === undefined){ +factory = dependencies; +if(toString.call(id) === '[object Array]'){ +dependencies = id; +id = _uid();}else +{ +dependencies = [];}} + + + + + +var canceler={cancel:_undefine.bind(this, id)}; + +var record=modulesMap[id]; + + + + + + +if(record){ +if(_refCount){ +record.refcount += _refCount;} + + +return canceler;}else +if(!dependencies && !factory && _refCount){ + + +predefinedRefCounts[id] = (predefinedRefCounts[id] || 0) + _refCount; +return canceler;}else +{ + +record = {id:id}; +record.refcount = (predefinedRefCounts[id] || 0) + (_refCount || 0); +delete predefinedRefCounts[id];} + + +if(__DEV__){ +if( +!factory || +typeof factory != 'object' && typeof factory != 'function' && +typeof factory != 'string'){ +throw new ModuleError( +'Invalid factory "' + factory + '" for module "' + id + '". ' + +'Factory should be either a function or an object.');} + + + +if(toString.call(dependencies) !== '[object Array]'){ +throw new ModuleError( +'Invalid dependencies for module "' + id + '". ' + +'Dependencies must be passed as an array.');}} + + + + +record.factory = factory; +record.dependencies = dependencies; +record.context = _context; +record.special = _special; +record.inlineRequires = _inlineRequires || {}; +record.waitingMap = {}; +record.waiting = 0; +record.hasError = false; +modulesMap[id] = record; +_initDependencies(id); + +return canceler;} + + +function _undefine(id){ +if(!modulesMap[id]){ +return;} + + +var module=modulesMap[id]; +delete modulesMap[id]; + +for(var dep in module.waitingMap) { +if(module.waitingMap[dep]){ +delete dependencyMap[dep][id];}} + + + +for(var ii=0; ii < module.dependencies.length; ii++) { +dep = module.dependencies[ii]; +if(modulesMap[dep]){ +if(modulesMap[dep].refcount-- === 1){ +_undefine(dep);}}else + +if(predefinedRefCounts[dep]){ +predefinedRefCounts[dep]--;}}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +function requireLazy(dependencies, factory, context){ +return define( +dependencies, +factory, +undefined, +REQUIRE_WHEN_READY, +context, +1);} + + + +function _uid(){ +return '__mod__' + _counter++;} + + +function _addDependency(module, dep){ + +if(!module.waitingMap[dep] && module.id !== dep){ +module.waiting++; +module.waitingMap[dep] = 1; +dependencyMap[dep] || (dependencyMap[dep] = {}); +dependencyMap[dep][module.id] = 1;}} + + + +function _initDependencies(id){ +var modulesToRequire=[]; +var module=modulesMap[id]; +var dep, i, subdep; + + +for(i = 0; i < module.dependencies.length; i++) { +dep = module.dependencies[i]; +if(!modulesMap[dep]){ +_addDependency(module, dep);}else +if(modulesMap[dep].waiting){ +for(subdep in modulesMap[dep].waitingMap) { +if(modulesMap[dep].waitingMap[subdep]){ +_addDependency(module, subdep);}}}} + + + + +if(module.waiting === 0 && module.special & REQUIRE_WHEN_READY){ +modulesToRequire.push(id);} + + + +if(dependencyMap[id]){ +var deps=dependencyMap[id]; +var submodule; +dependencyMap[id] = undefined; +for(dep in deps) { +submodule = modulesMap[dep]; + + +for(subdep in module.waitingMap) { +if(module.waitingMap[subdep]){ +_addDependency(submodule, subdep);}} + + + +if(submodule.waitingMap[id]){ +submodule.waitingMap[id] = undefined; +submodule.waiting--;} + +if(submodule.waiting === 0 && +submodule.special & REQUIRE_WHEN_READY){ +modulesToRequire.push(dep);}}} + + + + + +for(i = 0; i < modulesToRequire.length; i++) { +require.call(null, modulesToRequire[i]);}} + + + +function _register(id, exports){ +var module=modulesMap[id] = {id:id}; +module.exports = exports; +module.refcount = 0;} + + + + +_register('module', 0); +_register('exports', 0); + +_register('global', global); +_register('require', require); +_register('requireDynamic', require); +_register('requireLazy', requireLazy); + +global.require = require; +global.requireDynamic = require; +global.requireLazy = requireLazy; + +require.__debug = { +modules:modulesMap, +deps:dependencyMap, +printDependencyInfo:function(){ +if(!global.console){ +return;} + +var names=Object.keys(require.__debug.deps); +global.console.log(_debugUnresolvedDependencies(names));}}; + + + + + + + + + +global.__d = function(id, deps, factory, _special, _inlineRequires){ +var defaultDeps=['global', 'require', 'requireDynamic', 'requireLazy', +'module', 'exports']; +define(id, defaultDeps.concat(deps), factory, _special || USED_AS_TRANSPORT, +null, null, _inlineRequires);};})( + + +this); +Object. + + + + + + + + + + + + + + + + + +assign = function(target, sources){ +if(__DEV__){ +if(target == null){ +throw new TypeError('Object.assign target cannot be null or undefined');} + +if(typeof target !== 'object' && typeof target !== 'function'){ +throw new TypeError( +'In this environment the target of assign MUST be an object.' + +'This error is a performance optimization and not spec compliant.');}} + + + + +for(var nextIndex=1; nextIndex < arguments.length; nextIndex++) { +var nextSource=arguments[nextIndex]; +if(nextSource == null){ +continue;} + + +if(__DEV__){ +if(typeof nextSource !== 'object' && +typeof nextSource !== 'function'){ +throw new TypeError( +'In this environment the target of assign MUST be an object.' + +'This error is a performance optimization and not spec compliant.');}} + + + + + + + + +for(var key in nextSource) { +if(__DEV__){ +var hasOwnProperty=Object.prototype.hasOwnProperty; +if(!hasOwnProperty.call(nextSource, key)){ +throw new TypeError( +'One of the sources to assign has an enumerable key on the ' + +'prototype chain. This is an edge case that we do not support. ' + +'This error is a performance optimization and not spec compliant.');}} + + + +target[key] = nextSource[key];}} + + + +return target;}; +( + + + + + + + + + + + + + + + +function(global){ +'use strict'; + +var OBJECT_COLUMN_NAME='(index)'; +var LOG_LEVELS={ +trace:0, +log:1, +info:2, +warn:3, +error:4}; + + +function setupConsole(global){ + +if(!global.nativeLoggingHook){ +return;} + + +function getNativeLogFunction(level){ +return function(){ +var str=Array.prototype.map.call(arguments, function(arg){ +var ret; +var type=typeof arg; +if(arg === null){ +ret = 'null';}else +if(arg === undefined){ +ret = 'undefined';}else +if(type === 'string'){ +ret = '"' + arg + '"';}else +if(type === 'function'){ +try{ +ret = arg.toString();} +catch(e) { +ret = '[function unknown]';}}else + +{ + + +try{ +ret = JSON.stringify(arg);} +catch(e) { +if(typeof arg.toString === 'function'){ +try{ +ret = arg.toString();} +catch(E) {}}}} + + + +return ret || '["' + type + '" failed to stringify]';}). +join(', '); +global.nativeLoggingHook(str, level);};} + + + +var repeat=function(element, n){ +return Array.apply(null, Array(n)).map(function(){return element;});}; + + +function consoleTablePolyfill(rows){ + +if(!Array.isArray(rows)){ +var data=rows; +rows = []; +for(var key in data) { +if(data.hasOwnProperty(key)){ +var row=data[key]; +row[OBJECT_COLUMN_NAME] = key; +rows.push(row);}}} + + + +if(rows.length === 0){ +global.nativeLoggingHook('', LOG_LEVELS.log); +return;} + + +var columns=Object.keys(rows[0]).sort(); +var stringRows=[]; +var columnWidths=[]; + + + +columns.forEach(function(k, i){ +columnWidths[i] = k.length; +for(var j=0; j < rows.length; j++) { +var cellStr=rows[j][k].toString(); +stringRows[j] = stringRows[j] || []; +stringRows[j][i] = cellStr; +columnWidths[i] = Math.max(columnWidths[i], cellStr.length);}}); + + + + + +var joinRow=function(row, space){ +var cells=row.map(function(cell, i){ +var extraSpaces=repeat(' ', columnWidths[i] - cell.length).join(''); +return cell + extraSpaces;}); + +space = space || ' '; +return cells.join(space + '|' + space);}; + + +var separators=columnWidths.map(function(columnWidth){ +return repeat('-', columnWidth).join('');}); + +var separatorRow=joinRow(separators, '-'); +var header=joinRow(columns); +var table=[header, separatorRow]; + +for(var i=0; i < rows.length; i++) { +table.push(joinRow(stringRows[i]));} + + + + + + +global.nativeLoggingHook('\n' + table.join('\n'), LOG_LEVELS.log);} + + +global.console = { +error:getNativeLogFunction(LOG_LEVELS.error), +info:getNativeLogFunction(LOG_LEVELS.info), +log:getNativeLogFunction(LOG_LEVELS.log), +warn:getNativeLogFunction(LOG_LEVELS.warn), +trace:getNativeLogFunction(LOG_LEVELS.trace), +table:consoleTablePolyfill};} + + + + +if(typeof module !== 'undefined'){ +module.exports = setupConsole;}else +{ +setupConsole(global);}})( + + +this); +( + + + + + + + + + + + + + + + +function(global){ +var ErrorUtils={ +_inGuard:0, +_globalHandler:null, +setGlobalHandler:function(fun){ +ErrorUtils._globalHandler = fun;}, + +reportError:function(error){ +ErrorUtils._globalHandler && ErrorUtils._globalHandler(error);}, + +reportFatalError:function(error){ +ErrorUtils._globalHandler && ErrorUtils._globalHandler(error, true);}, + +applyWithGuard:function(fun, context, args){ +try{ +ErrorUtils._inGuard++; +return fun.apply(context, args);} +catch(e) { +ErrorUtils.reportError(e);}finally +{ +ErrorUtils._inGuard--;}}, + + +applyWithGuardIfNeeded:function(fun, context, args){ +if(ErrorUtils.inGuard()){ +return fun.apply(context, args);}else +{ +ErrorUtils.applyWithGuard(fun, context, args);}}, + + +inGuard:function(){ +return ErrorUtils._inGuard;}, + +guard:function(fun, name, context){ +if(typeof fun !== 'function'){ +console.warn('A function must be passed to ErrorUtils.guard, got ', fun); +return null;} + +name = name || fun.name || ''; +function guarded(){ +return ( +ErrorUtils.applyWithGuard( +fun, +context || this, +arguments, +null, +name));} + + + + +return guarded;}}; + + +global.ErrorUtils = ErrorUtils; + + + + + +function setupErrorGuard(){ +var onError=function(e){ +global.console.error( +'Error: ' + +'\n stack: ' + e.stack + +'\n line: ' + e.line + +'\n message: ' + e.message, +e);}; + + +global.ErrorUtils.setGlobalHandler(onError);} + + +setupErrorGuard();})( +this); +if( + + + + + + + + + + + +!String.prototype.startsWith){ +String.prototype.startsWith = function(search){ +'use strict'; +if(this == null){ +throw TypeError();} + +var string=String(this); +var pos=arguments.length > 1? +Number(arguments[1]) || 0:0; +var start=Math.min(Math.max(pos, 0), string.length); +return string.indexOf(String(search), pos) === start;};} + + + +if(!String.prototype.endsWith){ +String.prototype.endsWith = function(search){ +'use strict'; +if(this == null){ +throw TypeError();} + +var string=String(this); +var stringLength=string.length; +var searchString=String(search); +var pos=arguments.length > 1? +Number(arguments[1]) || 0:stringLength; +var end=Math.min(Math.max(pos, 0), stringLength); +var start=end - searchString.length; +if(start < 0){ +return false;} + +return string.lastIndexOf(searchString, start) === start;};} + + + +if(!String.prototype.contains){ +String.prototype.contains = function(search){ +'use strict'; +if(this == null){ +throw TypeError();} + +var string=String(this); +var pos=arguments.length > 1? +Number(arguments[1]) || 0:0; +return string.indexOf(String(search), pos) !== -1;};} + + + +if(!String.prototype.repeat){ +String.prototype.repeat = function(count){ +'use strict'; +if(this == null){ +throw TypeError();} + +var string=String(this); +count = Number(count) || 0; +if(count < 0 || count === Infinity){ +throw RangeError();} + +if(count === 1){ +return string;} + +var result=''; +while(count) { +if(count & 1){ +result += string;} + +if(count >>= 1){ +string += string;}} + + +return result;};} +( + + + + + + + + + +function(undefined){ + +function findIndex(predicate, context){ +if(this == null){ +throw new TypeError( +'Array.prototype.findIndex called on null or undefined');} + + +if(typeof predicate !== 'function'){ +throw new TypeError('predicate must be a function');} + +var list=Object(this); +var length=list.length >>> 0; +for(var i=0; i < length; i++) { +if(predicate.call(context, list[i], i, list)){ +return i;}} + + +return -1;} + + +if(!Array.prototype.findIndex){ +Object.defineProperty(Array.prototype, 'findIndex', { +enumerable:false, +writable:true, +configurable:true, +value:findIndex});} + + + + +if(!Array.prototype.find){ +Object.defineProperty(Array.prototype, 'find', { +enumerable:false, +writable:true, +configurable:true, +value:function(predicate, context){ +if(this == null){ +throw new TypeError( +'Array.prototype.find called on null or undefined');} + + +var index=findIndex.call(this, predicate, context); +return index === -1?undefined:this[index];}});}})(); +( +function(GLOBAL){ + + + + + + + +function getInvalidGlobalUseError(name){ +return new Error( +'You are trying to render the global ' + name + ' variable as a ' + +'React element. You probably forgot to require ' + name + '.');} + + +GLOBAL.Text = { +get defaultProps() { +throw getInvalidGlobalUseError('Text');}}; + + +GLOBAL.Image = { +get defaultProps() { +throw getInvalidGlobalUseError('Image');}}; + + + +if(GLOBAL.document){ +GLOBAL.document.createElement = null;} + + + + +GLOBAL.MutationObserver = undefined;})( +this); +__d('react-native/Examples/UIExplorer/UIExplorerUnitTests/Test.js',[],function(global, require, requireDynamic, requireLazy, module, exports) { +}); +;require("react-native/Examples/UIExplorer/UIExplorerUnitTests/Test.js"); +//@ sourceMappingURL=/Examples/UIExplorer/UIExplorerUnitTests/Test.includeRequire.runModule.map \ No newline at end of file diff --git a/Examples/UIExplorer/UIExplorerUnitTests/Test.js b/Examples/UIExplorer/UIExplorerUnitTests/Test.js new file mode 100644 index 000000000..3f0a21ff3 --- /dev/null +++ b/Examples/UIExplorer/UIExplorerUnitTests/Test.js @@ -0,0 +1,13 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ 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..0de78cb8f 100644 --- a/Libraries/ReactNative/ReactNativeViewAttributes.js +++ b/Libraries/ReactNative/ReactNativeViewAttributes.js @@ -24,6 +24,7 @@ ReactNativeViewAttributes.UIView = { onLayout: true, onAccessibilityTap: true, onMagicTap: true, + collapsible: 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 c6855fbf0..a69b8e7dd 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 @@ -464,6 +457,23 @@ 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 implementated. + + 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]; @@ -472,26 +482,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; } @@ -508,24 +522,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)]); @@ -537,13 +555,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); } @@ -565,7 +583,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) createAnimation.property); } for (RCTViewManagerUIBlock block in updateBlocks) { - block(self, _viewRegistry); + block(uiManager, viewRegistry); } } withCompletionBlock:nil]; } @@ -688,6 +706,126 @@ RCT_EXPORT_METHOD(replaceExistingNonRootView:(NSNumber *)reactTag withView:(NSNu removeAtIndices:removeAtIndices]; } +- (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:(out NSUInteger *)outOffset +{ + RCTShadowView *container = _shadowViewRegistry[containerReactTag]; + NSNumber *containerSuperviewReactTag = containerReactTag; + RCTShadowView *superview = container; + NSUInteger offset = 0; + + 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) { + 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; + } + + if (outOffset) *outOffset = offset; + return containerSuperviewReactTag; +} + RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices @@ -695,62 +833,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]; } } @@ -833,45 +1018,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 @@ -885,10 +1097,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; + 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,7 +1529,7 @@ RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { _jsResponder = viewRegistry[reactTag]; if (!_jsResponder) { - RCTLogError(@"Invalid view set to be the JS responder - tag %zd", reactTag); + RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag); } }]; } @@ -1485,3 +1787,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..a6c495498 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,82 @@ 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; + if (!layoutKeys) { + // Taken from LayoutPropTypes.js with the exception that borderWidth, + // borderTopWidth, borderBottomWidth, borderLeftWidth, and borderRightWidth + // were removed because black color is assumed + static NSString *const keys[] = { + @"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", + + // Special case is handled below. + @"collapsible", + }; + layoutKeys = [NSSet setWithObjects:keys count:sizeof(keys)/sizeof(*keys)]; + } + + NSNumber *collapsible = self.allProps[@"collapsible"]; + if (collapsible && !collapsible.boolValue) { + return NO; + } + + for (NSString *key in self.allProps) { + if (![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;