From 3c5b4b0a9f6bcc659ee4eb88dc7fc8765bea203e Mon Sep 17 00:00:00 2001 From: Alex Akers Date: Thu, 25 Jun 2015 09:07:32 -0700 Subject: [PATCH] [React Native] Remove layout-only nodes Summary: Remove layout-only views. Works by checking properties against a list of known properties that only affect layout. The `RCTShadowView` hierarchy still has a 1:1 correlation with the JS nodes. This works by adjusting the tags and indices in `manageChildren`. For example, if JS told us to insert tag 1 at index 0 and tag 1 is layout-only with children whose tags are 2 and 3, we adjust it so we insert tags 2 and 3 at indices 0 and 1. This keeps changes out of `RCTView` and `RCTScrollView`. In order to simplify this logic, view moves are now processed as view removals followed by additions. A move from index 0 to 1 is recorded as a removal of view at indices 0 and 1 and an insertion of tags 1 and 2 at indices 0 and 1. Of course, the remaining indices have to be offset to take account for this. The `collapsible` attribute is a bit of a hack to force `RCTScrollView` to always have one child. This was easier than rethinking out the logic there, but we could change this later. @public Test Plan: There are tests in `RCTUIManagerTests.m` that test the tag- and index-manipulation logic works. There are various scenarios including add-only, remove-only, and move. In addition, two scenario tests verify that the optimization works by checking the number of views and shadow views after various situations happen. --- .../UIExplorer.xcodeproj/project.pbxproj | 4 + .../testViewExampleSnapshot_1@2x.png | Bin 87728 -> 87728 bytes .../UIExplorerUnitTests/RCTUIManagerTests.m | 374 +++++- .../Test.includeRequire.runModule.bundle | 1120 +++++++++++++++++ .../UIExplorer/UIExplorerUnitTests/Test.js | 13 + Libraries/Components/ScrollView/ScrollView.js | 1 + Libraries/Components/View/View.js | 6 + .../ReactNative/ReactNativeViewAttributes.js | 1 + Libraries/Text/RCTShadowRawText.m | 5 + React/Modules/RCTUIManager.m | 520 ++++++-- React/Views/RCTShadowView.h | 6 + React/Views/RCTShadowView.m | 82 +- React/Views/RCTViewNodeProtocol.h | 3 +- 13 files changed, 2022 insertions(+), 113 deletions(-) create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/Test.includeRequire.runModule.bundle create mode 100644 Examples/UIExplorer/UIExplorerUnitTests/Test.js 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 d37b708794c43ab16e1fd7326207067117212cbc..cf264cbe726f09ea226f77bc6e8027cef106a28e 100644 GIT binary patch delta 18752 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 TPO=VMFw8*L^m64z=ez$4hE+VV delta 18707 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 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;