diff --git a/Examples/UIExplorer/ListViewGridLayoutExample.js b/Examples/UIExplorer/ListViewGridLayoutExample.js new file mode 100644 index 000000000..879b24387 --- /dev/null +++ b/Examples/UIExplorer/ListViewGridLayoutExample.js @@ -0,0 +1,149 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + ListView, + TouchableHighlight, + StyleSheet, + Text, + View, +} = React; + +var THUMB_URLS = [ + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/p128x128/851549_767334479959628_274486868_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851561_767334496626293_1958532586_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/p128x128/851579_767334503292959_179092627_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851589_767334513292958_1747022277_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851563_767334559959620_1193692107_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851593_767334566626286_1953955109_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851591_767334523292957_797560749_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851567_767334529959623_843148472_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851548_767334489959627_794462220_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851575_767334539959622_441598241_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-ash3/t39.1997/p128x128/851573_767334549959621_534583464_n.png', + 'https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-prn1/t39.1997/p128x128/851583_767334573292952_1519550680_n.png', +]; + +var ListViewGridLayoutExample = React.createClass({ + + statics: { + title: ' - Grid Layout', + description: 'Flexbox grid layout.' + }, + + getInitialState: function() { + var ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + return { + dataSource: ds.cloneWithRows(this._genRows({})), + }; + }, + + _pressData: ({}: {[key: number]: boolean}), + + componentWillMount: function() { + this._pressData = {}; + }, + + render: function() { + return ( + // ListView wraps ScrollView and so takes on its properties. + // With that in mind you can use the ScrollView's contentContainerStyle prop to style the items. + + ); + }, + + _renderRow: function(rowData: string, sectionID: number, rowID: number) { + var rowHash = Math.abs(hashCode(rowData)); + var imgSource = { + uri: THUMB_URLS[rowHash % THUMB_URLS.length], + }; + return ( + this._pressRow(rowID)} underlayColor="transparent"> + + + + + {rowData} + + + + + ); + }, + + _genRows: function(pressData: {[key: number]: boolean}): Array { + var dataBlob = []; + for (var ii = 0; ii < 100; ii++) { + var pressedText = pressData[ii] ? ' (X)' : ''; + dataBlob.push('Cell ' + ii + pressedText); + } + return dataBlob; + }, + + _pressRow: function(rowID: number) { + this._pressData[rowID] = !this._pressData[rowID]; + this.setState({dataSource: this.state.dataSource.cloneWithRows( + this._genRows(this._pressData) + )}); + }, +}); + +/* eslint no-bitwise: 0 */ +var hashCode = function(str) { + var hash = 15; + for (var ii = str.length - 1; ii >= 0; ii--) { + hash = ((hash << 5) - hash) + str.charCodeAt(ii); + } + return hash; +}; + +var styles = StyleSheet.create({ + list: { + justifyContent: 'space-around', + flexDirection: 'row', + flexWrap: 'wrap' + }, + row: { + justifyContent: 'center', + padding: 5, + margin: 3, + width: 100, + height: 100, + backgroundColor: '#F6F6F6', + alignItems: 'center', + borderWidth: 1, + borderRadius: 5, + borderColor: '#CCC' + }, + thumb: { + width: 64, + height: 64 + }, + text: { + flex: 1, + marginTop: 5, + fontWeight: 'bold' + }, +}); + +module.exports = ListViewGridLayoutExample; diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m index d6981fba2..c7d30539f 100644 --- a/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m +++ b/Examples/UIExplorer/UIExplorerIntegrationTests/RCTUIManagerScenarioTests.m @@ -15,46 +15,21 @@ #import #import - -#import "RCTShadowView.h" -#import "RCTUIManager.h" -#import "RCTRootView.h" #import "RCTSparseArray.h" +#import "RCTUIManager.h" #import "UIView+React.h" @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 @@ -77,360 +52,140 @@ 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; } } -/* +-----------------------------------------------------------+ +----------------------+ - * | 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 +- (void)testManagingChildrenToAddViews { - 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]; - }]; -} + UIView *containerView = _uiManager.viewRegistry[20]; + NSMutableArray *addedViews = [NSMutableArray array]; -- (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)DISABLED_testScenario1 -{ - RCTUIManager *uiManager = [[RCTUIManager alloc] init]; - RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[uiManager]; } launchOptions:nil]; - NS_VALID_UNTIL_END_OF_SCOPE RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Test"]; - - __block BOOL done = NO; - - 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); - done = YES; - }]; - - [uiManager flushUIBlocks]; - }); - - NSDate *date = [NSDate dateWithTimeIntervalSinceNow:1.0]; - while ([date timeIntervalSinceNow] > 0 && !done) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + NSArray *tagsToAdd = @[@1, @2, @3, @4, @5]; + NSArray *addAtIndices = @[@0, @1, @2, @3, @4]; + for (NSNumber *tag in tagsToAdd) { + [addedViews addObject:_uiManager.viewRegistry[tag]]; } - done = NO; - 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); - done = YES; - }]; + // Add views 1-5 to view 20 + [_uiManager _manageChildren:@20 + moveFromIndices:nil + moveToIndices:nil + addChildReactTags:tagsToAdd + addAtIndices:addAtIndices + removeAtIndices:nil + registry:_uiManager.viewRegistry]; - [uiManager flushUIBlocks]; - }); - - date = [NSDate dateWithTimeIntervalSinceNow:1.0]; - while ([date timeIntervalSinceNow] > 0 && !done) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - } - - done = NO; - 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); - done = YES; - }]; - - [uiManager flushUIBlocks]; - }); - - date = [NSDate dateWithTimeIntervalSinceNow:1.0]; - while ([date timeIntervalSinceNow] > 0 && !done) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + XCTAssertTrue([[containerView reactSubviews] count] == 5, + @"Expect to have 5 react subviews after calling manage children \ + with 5 tags to add, instead have %lu", (unsigned long)[[containerView reactSubviews] count]); + for (UIView *view in addedViews) { + XCTAssertTrue([view superview] == containerView, + @"Expected to have manage children successfully add children"); + [view removeFromSuperview]; } } -- (void)DISABLED_testScenario2 +- (void)testManagingChildrenToRemoveViews { - RCTUIManager *uiManager = [[RCTUIManager alloc] init]; - RCTBridge *bridge = [[RCTBridge alloc] initWithBundleURL:nil moduleProvider:^{ return @[uiManager]; } launchOptions:nil]; - NS_VALID_UNTIL_END_OF_SCOPE RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Test"]; + UIView *containerView = _uiManager.viewRegistry[20]; + NSMutableArray *removedViews = [NSMutableArray array]; - __block BOOL done = NO; - - 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); - done = YES; - }]; - - [uiManager flushUIBlocks]; - }); - - NSDate *date = [NSDate dateWithTimeIntervalSinceNow:1.0]; - while ([date timeIntervalSinceNow] > 0 && !done) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + NSArray *removeAtIndices = @[@0, @4, @8, @12, @16]; + for (NSNumber *index in removeAtIndices) { + NSNumber *reactTag = @([index integerValue] + 2); + [removedViews addObject:_uiManager.viewRegistry[reactTag]]; + } + for (NSInteger i = 2; i < 20; i++) { + UIView *view = _uiManager.viewRegistry[i]; + [containerView addSubview:view]; } - done = NO; - 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); - done = YES; - }]; + // Remove views 1-5 from view 20 + [_uiManager _manageChildren:@20 + moveFromIndices:nil + moveToIndices:nil + addChildReactTags:nil + addAtIndices:nil + removeAtIndices:removeAtIndices + registry:_uiManager.viewRegistry]; - [uiManager flushUIBlocks]; - }); + XCTAssertEqual(containerView.reactSubviews.count, (NSUInteger)13, + @"Expect to have 13 react subviews after calling manage children\ + with 5 tags to remove and 18 prior children, instead have %zd", + containerView.reactSubviews.count); + for (UIView *view in removedViews) { + XCTAssertTrue([view superview] == nil, + @"Expected to have manage children successfully remove children"); + // After removing views are unregistered - we need to reregister + _uiManager.viewRegistry[view.reactTag] = view; + } + for (NSInteger i = 2; i < 20; i++) { + UIView *view = _uiManager.viewRegistry[i]; + if (![removedViews containsObject:view]) { + XCTAssertTrue([view superview] == containerView, + @"Should not have removed view with react tag %ld during delete but did", (long)i); + [view removeFromSuperview]; + } + } +} - date = [NSDate dateWithTimeIntervalSinceNow:1.0]; - while ([date timeIntervalSinceNow] > 0 && !done) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; +// We want to start with views 1-10 added at indices 0-9 +// Then we'll remove indices 2, 3, 5 and 8 +// Add views 11 and 12 to indices 0 and 6 +// And move indices 4 and 9 to 1 and 7 +// So in total it goes from: +// [1,2,3,4,5,6,7,8,9,10] +// to +// [11,5,1,2,7,8,12,10] +- (void)testManagingChildrenToAddRemoveAndMove +{ + 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]; + + // We need to keep these in array to keep them around + NSMutableArray *viewsToRemove = [NSMutableArray array]; + for (NSUInteger i = 0; i < removeAtIndices.count; i++) { + NSNumber *reactTagToRemove = @([removeAtIndices[i] integerValue] + 1); + UIView *viewToRemove = _uiManager.viewRegistry[reactTagToRemove]; + [viewsToRemove addObject:viewToRemove]; } - done = NO; - 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); - done = YES; - }]; + for (NSInteger i = 1; i < 11; i++) { + UIView *view = _uiManager.viewRegistry[i]; + [containerView addSubview:view]; + } - [uiManager flushUIBlocks]; - }); + [_uiManager _manageChildren:@20 + moveFromIndices:moveFromIndices + moveToIndices:moveToIndices + addChildReactTags:tagsToAdd + addAtIndices:addAtIndices + removeAtIndices:removeAtIndices + registry:_uiManager.viewRegistry]; - date = [NSDate dateWithTimeIntervalSinceNow:1.0]; - while ([date timeIntervalSinceNow] > 0 && !done) { - [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; - [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + XCTAssertTrue([[containerView reactSubviews] count] == 8, + @"Expect to have 8 react subviews after calling manage children,\ + instead have the following subviews %@", [containerView reactSubviews]); + + NSArray *expectedReactTags = @[@11, @5, @1, @2, @7, @8, @12, @10]; + for (NSUInteger i = 0; i < containerView.subviews.count; i++) { + XCTAssertEqualObjects([[containerView reactSubviews][i] reactTag], expectedReactTags[i], + @"Expected subview at index %ld to have react tag #%@ but has tag #%@", + (long)i, expectedReactTags[i], [[containerView reactSubviews][i] reactTag]); + } + + // Clean up after ourselves + for (NSInteger i = 1; i < 13; i++) { + UIView *view = _uiManager.viewRegistry[i]; + [view removeFromSuperview]; + } + for (UIView *view in viewsToRemove) { + _uiManager.viewRegistry[view.reactTag] = view; } } diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png index 2451b5f92..fb2ab9feb 100644 Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-UIExplorerApp.ios/testViewExample_1@2x.png differ diff --git a/Examples/UIExplorer/UIExplorerList.js b/Examples/UIExplorer/UIExplorerList.js index 20edd13b0..c3fc0492a 100644 --- a/Examples/UIExplorer/UIExplorerList.js +++ b/Examples/UIExplorer/UIExplorerList.js @@ -40,6 +40,7 @@ var COMMON_COMPONENTS = [ require('./ImageExample'), require('./LayoutEventsExample'), require('./ListViewExample'), + require('./ListViewGridLayoutExample'), require('./ListViewPagingExample'), require('./MapViewExample'), require('./Navigator/NavigatorExample'), diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m index 14cc34484..8fc629fbe 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTAllocationTests.m @@ -19,17 +19,15 @@ #import "RCTContextExecutor.h" #import "RCTRootView.h" -#define RUN_RUNLOOP_WHILE(CONDITION, TIMEOUT) \ +#define RUN_RUNLOOP_WHILE(CONDITION) \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wshadow\"") \ -NSDate *timeout = [[NSDate date] dateByAddingTimeInterval:TIMEOUT]; \ +NSDate *timeout = [[NSDate date] dateByAddingTimeInterval:0.1]; \ while ((CONDITION) && [timeout timeIntervalSinceNow] > 0) { \ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeout]; \ } \ _Pragma("clang diagnostic pop") -#define DEFAULT_TIMEOUT 2 - @interface RCTBridge (RCTAllocationTests) @property (nonatomic, weak) RCTBridge *batchedBridge; @@ -83,7 +81,6 @@ RCT_EXPORT_MODULE(); (void)view; } - sleep(DEFAULT_TIMEOUT); XCTAssertNil(weakBridge, @"RCTBridge should have been deallocated"); } @@ -104,8 +101,7 @@ RCT_EXPORT_MODULE(); * Sleep on the main thread to allow js thread deallocations then run the runloop * to allow the module to be deallocated on the main thread */ - sleep(1); - RUN_RUNLOOP_WHILE(module.isValid, 1) + RUN_RUNLOOP_WHILE(module.isValid) XCTAssertFalse(module.isValid, @"AllocationTestModule should have been invalidated by the bridge"); } @@ -124,12 +120,7 @@ RCT_EXPORT_MODULE(); (void)bridge; } - /** - * Sleep on the main thread to allow js thread deallocations then run the runloop - * to allow the module to be deallocated on the main thread - */ - sleep(1); - RUN_RUNLOOP_WHILE(weakModule, 1) + RUN_RUNLOOP_WHILE(weakModule) XCTAssertNil(weakModule, @"AllocationTestModule should have been deallocated"); } @@ -145,8 +136,7 @@ RCT_EXPORT_MODULE(); (void)bridge; } - RUN_RUNLOOP_WHILE(weakExecutor, 1); - sleep(1); + RUN_RUNLOOP_WHILE(weakExecutor); XCTAssertNil(weakExecutor, @"JavaScriptExecutor should have been released"); } @@ -158,13 +148,12 @@ RCT_EXPORT_MODULE(); moduleProvider:nil launchOptions:nil]; id executor = [bridge.batchedBridge valueForKey:@"javaScriptExecutor"]; - RUN_RUNLOOP_WHILE(!(weakContext = [executor valueForKey:@"context"]), DEFAULT_TIMEOUT); + RUN_RUNLOOP_WHILE(!(weakContext = [executor valueForKey:@"context"])); XCTAssertNotNil(weakContext, @"RCTJavaScriptContext should have been created"); (void)bridge; } - RUN_RUNLOOP_WHILE(weakContext, 1); - sleep(1); + RUN_RUNLOOP_WHILE(weakContext); XCTAssertNil(weakContext, @"RCTJavaScriptContext should have been deallocated"); } @@ -176,12 +165,11 @@ RCT_EXPORT_MODULE(); __weak id rootContentView; @autoreleasepool { RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@""]; - RUN_RUNLOOP_WHILE(!(rootContentView = [rootView valueForKey:@"contentView"]), DEFAULT_TIMEOUT) + RUN_RUNLOOP_WHILE(!(rootContentView = [rootView valueForKey:@"contentView"])) XCTAssertTrue([rootContentView isValid], @"RCTContentView should be valid"); (void)rootView; } - sleep(DEFAULT_TIMEOUT); XCTAssertFalse([rootContentView isValid], @"RCTContentView should have been invalidated"); } @@ -196,8 +184,7 @@ RCT_EXPORT_MODULE(); [bridge reload]; } - // Use RUN_RUNLOOP_WHILE because `batchedBridge` deallocates on the main thread. - RUN_RUNLOOP_WHILE(batchedBridge != nil, DEFAULT_TIMEOUT) + RUN_RUNLOOP_WHILE(batchedBridge != nil) XCTAssertNotNil(bridge, @"RCTBridge should not have been deallocated"); XCTAssertNil(batchedBridge, @"RCTBatchedBridge should have been deallocated"); diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m index 18b4118ba..f7db2d46f 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTContextExecutorTests.m @@ -19,6 +19,7 @@ #import "RCTContextExecutor.h" #import "RCTUtils.h" +#define RUN_PERF_TESTS 0 @interface RCTContextExecutorTests : XCTestCase @@ -48,6 +49,8 @@ [_executor invalidate]; } +#if RUN_PERF_TESTS + static uint64_t _get_time_nanoseconds(void) { static struct mach_timebase_info tb_info = {0, 0}; @@ -91,7 +94,7 @@ static uint64_t _get_time_nanoseconds(void) JSContextGroupRelease(group); } -- (void)MANUALLY_testJavaScriptCallSpeed +- (void)testJavaScriptCallSpeed { /** * Since we almost don't change the RCTContextExecutor logic, and this test is @@ -200,4 +203,6 @@ static uint64_t _get_time_nanoseconds(void) } } +#endif + @end diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m index 0e9290814..b587d9bca 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTUIManagerTests.m @@ -14,8 +14,6 @@ #import -#import "RCTRootView.h" -#import "RCTShadowView.h" #import "RCTSparseArray.h" #import "RCTUIManager.h" #import "UIView+React.h" @@ -23,13 +21,14 @@ @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; @property (nonatomic, readonly) RCTSparseArray *viewRegistry; -@property (nonatomic, readonly) RCTSparseArray *shadowViewRegistry; // RCT thread only @end @@ -52,11 +51,6 @@ 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; } } @@ -73,6 +67,8 @@ // Add views 1-5 to view 20 [_uiManager _manageChildren:@20 + moveFromIndices:nil + moveToIndices:nil addChildReactTags:tagsToAdd addAtIndices:addAtIndices removeAtIndices:nil @@ -105,6 +101,8 @@ // Remove views 1-5 from view 20 [_uiManager _manageChildren:@20 + moveFromIndices:nil + moveToIndices:nil addChildReactTags:nil addAtIndices:nil removeAtIndices:removeAtIndices @@ -142,9 +140,11 @@ { UIView *containerView = _uiManager.viewRegistry[20]; - NSArray *removeAtIndices = @[@2, @3, @5, @8, @4, @9]; - NSArray *addAtIndices = @[@0, @6, @1, @7]; - NSArray *tagsToAdd = @[@11, @12, @5, @10]; + NSArray *removeAtIndices = @[@2, @3, @5, @8]; + NSArray *addAtIndices = @[@0, @6]; + NSArray *tagsToAdd = @[@11, @12]; + NSArray *moveFromIndices = @[@4, @9]; + NSArray *moveToIndices = @[@1, @7]; // We need to keep these in array to keep them around NSMutableArray *viewsToRemove = [NSMutableArray array]; @@ -160,6 +160,8 @@ } [_uiManager _manageChildren:@20 + moveFromIndices:moveFromIndices + moveToIndices:moveToIndices addChildReactTags:tagsToAdd addAtIndices:addAtIndices removeAtIndices:removeAtIndices diff --git a/Libraries/Components/Navigation/NavigatorIOS.ios.js b/Libraries/Components/Navigation/NavigatorIOS.ios.js index 053ab0ee3..5d47b4df6 100644 --- a/Libraries/Components/Navigation/NavigatorIOS.ios.js +++ b/Libraries/Components/Navigation/NavigatorIOS.ios.js @@ -60,6 +60,7 @@ var RCTNavigatorItem = createReactNativeComponentClass({ tintColor: true, translucent: true, navigationBarHidden: true, + shadowHidden: true, titleTextColor: true, style: true, }, @@ -281,6 +282,11 @@ var NavigatorIOS = React.createClass({ */ navigationBarHidden: PropTypes.bool, + /** + * A Boolean value that indicates whether to hide the 1px hairline shadow + */ + shadowHidden: PropTypes.bool, + /** * The default wrapper style for components in the navigator. * A common use case is to set the backgroundColor for every page @@ -640,6 +646,7 @@ var NavigatorIOS = React.createClass({ rightButtonTitle={route.rightButtonTitle} onNavRightButtonTap={route.onRightButtonPress} navigationBarHidden={this.props.navigationBarHidden} + shadowHidden={this.props.shadowHidden} tintColor={this.props.tintColor} barTintColor={this.props.barTintColor} translucent={this.props.translucent !== false} diff --git a/Libraries/Components/ScrollView/ScrollView.js b/Libraries/Components/ScrollView/ScrollView.js index 2625c20b7..512d8dbd6 100644 --- a/Libraries/Components/ScrollView/ScrollView.js +++ b/Libraries/Components/ScrollView/ScrollView.js @@ -17,7 +17,6 @@ var PointPropType = require('PointPropType'); var RCTScrollView = require('NativeModules').UIManager.RCTScrollView; var RCTScrollViewConsts = RCTScrollView.Constants; var React = require('React'); -var ReactChildren = require('ReactChildren'); var ReactNativeViewAttributes = require('ReactNativeViewAttributes'); var RCTUIManager = require('NativeModules').UIManager; var ScrollResponder = require('ScrollResponder'); @@ -278,24 +277,12 @@ var ScrollView = React.createClass({ } } - var children = this.props.children; - if (this.props.stickyHeaderIndices) { - children = ReactChildren.map(children, (child) => { - if (child) { - return {child}; - } else { - return child; - } - }); - } - var contentContainer = - {children} + {this.props.children} ; var alwaysBounceHorizontal = diff --git a/Libraries/Components/TextInput/TextInput.js b/Libraries/Components/TextInput/TextInput.js index 141cefab0..cc1b00b41 100644 --- a/Libraries/Components/TextInput/TextInput.js +++ b/Libraries/Components/TextInput/TextInput.js @@ -537,7 +537,9 @@ var TextInput = React.createClass({ }, _onPress: function(event: Event) { - this.focus(); + if (this.props.editable || this.props.editable === undefined) { + this.focus(); + } }, _onChange: function(event: Event) { diff --git a/Libraries/Components/View/View.js b/Libraries/Components/View/View.js index 6912c05db..707d97b7e 100644 --- a/Libraries/Components/View/View.js +++ b/Libraries/Components/View/View.js @@ -83,12 +83,6 @@ 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/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 16faeb100..93ba01f34 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -209,7 +209,7 @@ var ListView = React.createClass({ */ getMetrics: function() { return { - contentHeight: this.scrollProperties.contentHeight, + contentLength: this.scrollProperties.contentLength, totalRows: this.props.dataSource.getRowCount(), renderedRows: this.state.curRenderedRowsCount, visibleRows: Object.keys(this._visibleRows).length, @@ -255,9 +255,9 @@ var ListView = React.createClass({ componentWillMount: function() { // this data should never trigger a render pass, so don't put in state this.scrollProperties = { - visibleHeight: null, - contentHeight: null, - offsetY: 0 + visibleLength: null, + contentLength: null, + offset: 0 }; this._childFrames = []; this._visibleRows = {}; @@ -409,12 +409,12 @@ var ListView = React.createClass({ scrollComponent.getInnerViewNode(), React.findNodeHandle(scrollComponent), logError, - this._setScrollContentHeight + this._setScrollContentLength ); RCTUIManager.measureLayoutRelativeToParent( React.findNodeHandle(scrollComponent), logError, - this._setScrollVisibleHeight + this._setScrollVisibleLength ); // RCTScrollViewManager.calculateChildFrames is not available on @@ -426,12 +426,14 @@ var ListView = React.createClass({ ); }, - _setScrollContentHeight: function(left, top, width, height) { - this.scrollProperties.contentHeight = height; + _setScrollContentLength: function(left, top, width, height) { + this.scrollProperties.contentLength = !this.props.horizontal ? + height : width; }, - _setScrollVisibleHeight: function(left, top, width, height) { - this.scrollProperties.visibleHeight = height; + _setScrollVisibleLength: function(left, top, width, height) { + this.scrollProperties.visibleLength = !this.props.horizontal ? + height : width; this._updateVisibleRows(); this._renderMoreRowsIfNeeded(); }, @@ -441,8 +443,8 @@ var ListView = React.createClass({ }, _renderMoreRowsIfNeeded: function() { - if (this.scrollProperties.contentHeight === null || - this.scrollProperties.visibleHeight === null || + if (this.scrollProperties.contentLength === null || + this.scrollProperties.visibleLength === null || this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) { return; } @@ -472,9 +474,9 @@ var ListView = React.createClass({ }, _getDistanceFromEnd: function(scrollProperties) { - return scrollProperties.contentHeight - - scrollProperties.visibleHeight - - scrollProperties.offsetY; + return scrollProperties.contentLength - + scrollProperties.visibleLength - + scrollProperties.offset; }, _updateVisibleRows: function(updatedFrames) { @@ -486,9 +488,10 @@ var ListView = React.createClass({ this._childFrames[newFrame.index] = merge(newFrame); }); } + var isVertical = !this.props.horizontal; var dataSource = this.props.dataSource; - var visibleTop = this.scrollProperties.offsetY; - var visibleBottom = visibleTop + this.scrollProperties.visibleHeight; + var visibleMin = this.scrollProperties.offset; + var visibleMax = visibleMin + this.scrollProperties.visibleLength; var allRowIDs = dataSource.rowIdentities; var header = this.props.renderHeader && this.props.renderHeader(); @@ -516,9 +519,9 @@ var ListView = React.createClass({ break; } var rowVisible = visibleSection[rowID]; - var top = frame.y; - var bottom = top + frame.height; - if (top > visibleBottom || bottom < visibleTop) { + var min = isVertical ? frame.y : frame.x; + var max = min + (isVertical ? frame.height : frame.width); + if (min > visibleMax || max < visibleMin) { if (rowVisible) { visibilityChanged = true; delete visibleSection[rowID]; @@ -546,16 +549,23 @@ var ListView = React.createClass({ }, _onScroll: function(e) { - this.scrollProperties.visibleHeight = e.nativeEvent.layoutMeasurement.height; - this.scrollProperties.contentHeight = e.nativeEvent.contentSize.height; - this.scrollProperties.offsetY = e.nativeEvent.contentOffset.y; + var isVertical = !this.props.horizontal; + this.scrollProperties.visibleLength = e.nativeEvent.layoutMeasurement[ + isVertical ? 'height' : 'width' + ]; + this.scrollProperties.contentLength = e.nativeEvent.contentSize[ + isVertical ? 'height' : 'width' + ]; + this.scrollProperties.offset = e.nativeEvent.contentOffset[ + isVertical ? 'y' : 'x' + ]; this._updateVisibleRows(e.nativeEvent.updatedChildFrames); var nearEnd = this._getDistanceFromEnd(this.scrollProperties) < this.props.onEndReachedThreshold; if (nearEnd && this.props.onEndReached && - this.scrollProperties.contentHeight !== this._sentEndForContentHeight && + this.scrollProperties.contentLength !== this._sentEndForContentLength && this.state.curRenderedRowsCount === this.props.dataSource.getRowCount()) { - this._sentEndForContentHeight = this.scrollProperties.contentHeight; + this._sentEndForContentLength = this.scrollProperties.contentLength; this.props.onEndReached(e); } else { this._renderMoreRowsIfNeeded(); diff --git a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js index b6923b4f2..343e1f3e6 100644 --- a/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js +++ b/Libraries/CustomComponents/Navigator/Navigation/NavigationEvent.js @@ -1,20 +1,119 @@ /** - * Copyright 2004-present Facebook. All Rights Reserved. + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @providesModule NavigationEvent * @flow */ 'use strict'; +var invariant = require('invariant'); + +class NavigationEventPool { + _list: Array; + + constructor() { + this._list = []; + } + + get(type: String, target: Object, data: any): NavigationEvent { + var event; + if (this._list.length > 0) { + event = this._list.pop(); + event.constructor.call(event, type, target, data); + } else { + event = new NavigationEvent(type, target, data); + } + return event; + } + + put(event: NavigationEvent) { + this._list.push(event); + } +} + +var _navigationEventPool = new NavigationEventPool(); + class NavigationEvent { - type: String; - target: Object; - data: any; + _data: any; + _defaultPrevented: boolean; + _disposed: boolean; + _target: ?Object; + _type: ?String; + + static pool(type: String, target: Object, data: any): NavigationEvent { + return _navigationEventPool.get(type, target, data); + } constructor(type: String, target: Object, data: any) { - this.type = type; - this.target = target; - this.data = data; + this._type = type; + this._target = target; + this._data = data; + this._defaultPrevented = false; + this._disposed = false; + } + + /* $FlowFixMe - get/set properties not yet supported */ + get type(): string { + return this._type; + } + + /* $FlowFixMe - get/set properties not yet supported */ + get target(): Object { + return this._target; + } + + /* $FlowFixMe - get/set properties not yet supported */ + get data(): any { + return this._data; + } + + /* $FlowFixMe - get/set properties not yet supported */ + get defaultPrevented(): boolean { + return this._defaultPrevented; + } + + preventDefault(): void { + this._defaultPrevented = true; + } + + /** + * Dispose the event. + * NavigationEvent shall be disposed after being emitted by + * `NavigationEventEmitter`. + */ + dispose(): void { + invariant(!this._disposed, 'NavigationEvent is already disposed'); + this._disposed = true; + + // Clean up. + this._type = null; + this._target = null; + this._data = null; + this._defaultPrevented = false; + + // Put this back to the pool to reuse the instance. + _navigationEventPool.put(this); } } diff --git a/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEvent-test.js b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEvent-test.js new file mode 100644 index 000000000..8ae24ddb4 --- /dev/null +++ b/Libraries/CustomComponents/Navigator/Navigation/__tests__/NavigationEvent-test.js @@ -0,0 +1,71 @@ + +/** + * Copyright (c) 2015, Facebook, Inc. All rights reserved. + * + * Facebook, Inc. (“Facebook”) owns all right, title and interest, including + * all intellectual property and other proprietary rights, in and to the React + * Native CustomComponents software (the “Software”). Subject to your + * compliance with these terms, you are hereby granted a non-exclusive, + * worldwide, royalty-free copyright license to (1) use and copy the Software; + * and (2) reproduce and distribute the Software as part of your own software + * (“Your Software”). Facebook reserves all rights not expressly granted to + * you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. + * IN NO EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICERS, DIRECTORS OR + * EMPLOYEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +'use strict'; + +jest + .dontMock('NavigationEvent') + .dontMock('invariant'); + +var NavigationEvent = require('NavigationEvent'); + +describe('NavigationEvent', () => { + it('constructs', () => { + var target = {}; + var event = new NavigationEvent('foo', target, 123); + expect(event.type).toBe('foo'); + expect(event.target).toBe(target); + expect(event.data).toBe(123); + }); + + it('constructs from pool', () => { + var target = {}; + var event = NavigationEvent.pool('foo', target, 123); + expect(event.type).toBe('foo'); + expect(event.target).toBe(target); + expect(event.data).toBe(123); + }); + + it('prevents default', () => { + var event = new NavigationEvent('foo', {}, 123); + expect(event.defaultPrevented).toBe(false); + event.preventDefault(); + expect(event.defaultPrevented).toBe(true); + }); + + it('recycles', () => { + var event1 = NavigationEvent.pool('foo', {}, 123); + event1.dispose(); + expect(event1.type).toBe(null); + expect(event1.data).toBe(null); + expect(event1.target).toBe(null); + + var event2 = NavigationEvent.pool('bar', {}, 456); + expect(event2.type).toBe('bar'); + expect(event2).toBe(event1); + }); +}); + + diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index 573223771..bc5d65791 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -220,6 +220,11 @@ var Navigator = React.createClass({ */ onDidFocus: PropTypes.func, + /** + * Will be called with (ref, indexInStack, route) when the scene ref changes + */ + onItemRef: PropTypes.func, + /** * Optionally provide a navigation bar that persists across scene * transitions @@ -313,6 +318,7 @@ var Navigator = React.createClass({ onPanResponderMove: this._handlePanResponderMove, onPanResponderTerminate: this._handlePanResponderTerminate, }); + this._itemRefs = {}; this._interactionHandle = null; this._emitWillFocus(this.state.routeStack[this.state.presentedIndex]); }, @@ -1000,10 +1006,22 @@ var Navigator = React.createClass({ return this.state.routeStack.slice(); }, + _handleItemRef: function(itemId, route, ref) { + this._itemRefs[itemId] = ref; + var itemIndex = this.state.idStack.indexOf(itemId); + if (itemIndex === -1) { + return; + } + this.props.onItemRef && this.props.onItemRef(ref, itemIndex, route); + }, + _cleanScenesPastIndex: function(index) { var newStackLength = index + 1; // Remove any unneeded rendered routes. if (newStackLength < this.state.routeStack.length) { + this.state.idStack.slice(newStackLength).map((removingId) => { + this._itemRefs[removingId] = null; + }); this.setState({ sceneConfigStack: this.state.sceneConfigStack.slice(0, newStackLength), idStack: this.state.idStack.slice(0, newStackLength), @@ -1013,22 +1031,38 @@ var Navigator = React.createClass({ }, _renderScene: function(route, i) { + var child = this.props.renderScene( + route, + this + ); var disabledSceneStyle = null; if (i !== this.state.presentedIndex) { disabledSceneStyle = styles.disabledScene; } + var originalRef = child.ref; + if (originalRef != null && typeof originalRef !== 'function') { + console.warn( + 'String refs are not supported for navigator scenes. Use a callback ' + + 'ref instead. Ignoring ref: ' + originalRef + ); + originalRef = null; + } return ( { return (this.state.transitionFromIndex != null) || (this.state.transitionFromIndex != null); }} style={[styles.baseScene, this.props.sceneStyle, disabledSceneStyle]}> - {this.props.renderScene( - route, - this - )} + {React.cloneElement(child, { + ref: component => { + this._handleItemRef(this.state.idStack[i], route, component); + if (originalRef) { + originalRef(component); + } + } + })} ); }, diff --git a/Libraries/Network/RCTNetworking.m b/Libraries/Network/RCTNetworking.m index b6fb100d7..d86b00950 100644 --- a/Libraries/Network/RCTNetworking.m +++ b/Libraries/Network/RCTNetworking.m @@ -249,8 +249,15 @@ RCT_EXPORT_MODULE() request.HTTPBody = result[@"body"]; NSString *contentType = result[@"contentType"]; if (contentType) { - [request setValue:contentType forHTTPHeaderField:@"content-type"]; + [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; } + + // Gzip the request body + if ([request.allHTTPHeaderFields[@"Content-Encoding"] isEqualToString:@"gzip"]) { + request.HTTPBody = RCTGzipData(request.HTTPBody, -1 /* default */); + [request setValue:[@(request.HTTPBody.length) description] forHTTPHeaderField:@"Content-Length"]; + } + [self sendRequest:request incrementalUpdates:incrementalUpdates responseSender:responseSender]; diff --git a/Libraries/ReactNative/ReactNativeViewAttributes.js b/Libraries/ReactNative/ReactNativeViewAttributes.js index 367c6be2d..50b839e1d 100644 --- a/Libraries/ReactNative/ReactNativeViewAttributes.js +++ b/Libraries/ReactNative/ReactNativeViewAttributes.js @@ -24,22 +24,6 @@ ReactNativeViewAttributes.UIView = { onLayout: true, onAccessibilityTap: true, onMagicTap: true, - collapsible: true, - - // If editing layout-only view attributes, make sure - // -[RCTShadowView isLayoutOnly] in RCTShadowView.m - // is up-to-date! If any property below is set, the - // view should not be collapsible, but this is done - // on the native side. - onMoveShouldSetResponder: true, - onResponderGrant: true, - onResponderMove: true, - onResponderReject: true, - onResponderRelease: true, - onResponderTerminate: true, - onResponderTerminationRequest: true, - onStartShouldSetResponder: true, - onStartShouldSetResponderCapture: true, }; ReactNativeViewAttributes.RCTView = merge( diff --git a/Libraries/Text/RCTShadowRawText.m b/Libraries/Text/RCTShadowRawText.m index 00a3490bc..e99e1187b 100644 --- a/Libraries/Text/RCTShadowRawText.m +++ b/Libraries/Text/RCTShadowRawText.m @@ -20,11 +20,6 @@ } } -- (BOOL)isLayoutOnly -{ - return YES; -} - - (NSString *)description { NSString *superDescription = super.description; diff --git a/Libraries/Utilities/MatrixMath.js b/Libraries/Utilities/MatrixMath.js index c6a26ea82..6103e1d1e 100755 --- a/Libraries/Utilities/MatrixMath.js +++ b/Libraries/Utilities/MatrixMath.js @@ -31,6 +31,24 @@ var MatrixMath = { ]; }, + createFrustum: function(left, right, bottom, top, near, far) { + var r_width = 1 / (right - left); + var r_height = 1 / (top - bottom); + var r_depth = 1 / (near - far); + var x = 2 * (near * r_width); + var y = 2 * (near * r_height); + var A = (right + left) * r_width; + var B = (top + bottom) * r_height; + var C = (far + near) * r_depth; + var D = 2 * (far * near * r_depth); + return [ + x, 0, 0, 0, + 0, y, 0, 0, + A, B, C,-1, + 0, 0, D, 0, + ]; + }, + createTranslate2d: function(x, y) { var mat = MatrixMath.createIdentityMatrix(); MatrixMath.reuseTranslate2dCommand(mat, x, y); diff --git a/Libraries/Utilities/PerformanceLogger.js b/Libraries/Utilities/PerformanceLogger.js index c05c4b7c2..c7f0e316c 100644 --- a/Libraries/Utilities/PerformanceLogger.js +++ b/Libraries/Utilities/PerformanceLogger.js @@ -24,7 +24,8 @@ var PerformanceLogger = { if (timespans[key]) { if (__DEV__) { console.log( - 'PerformanceLogger: Attempting to add a timespan that already exists' + 'PerformanceLogger: Attempting to add a timespan that already exists ', + key ); } return; @@ -40,7 +41,8 @@ var PerformanceLogger = { if (timespans[key]) { if (__DEV__) { console.log( - 'PerformanceLogger: Attempting to start a timespan that already exists' + 'PerformanceLogger: Attempting to start a timespan that already exists ', + key, ); } return; @@ -56,7 +58,8 @@ var PerformanceLogger = { if (!timespans[key] || !timespans[key].startTime) { if (__DEV__) { console.log( - 'PerformanceLogger: Attempting to end a timespan that has not started' + 'PerformanceLogger: Attempting to end a timespan that has not started ', + key, ); } return; @@ -75,6 +78,10 @@ var PerformanceLogger = { return timespans; }, + hasTimespan(key) { + return !!timespans[key]; + }, + logTimespans() { for (var key in timespans) { console.log(key + ': ' + timespans[key].totalTime + 'ms'); diff --git a/React/Base/RCTUtils.h b/React/Base/RCTUtils.h index e21a07222..23926aa71 100644 --- a/React/Base/RCTUtils.h +++ b/React/Base/RCTUtils.h @@ -15,7 +15,7 @@ #import "RCTAssert.h" #import "RCTDefines.h" -// Utility functions for JSON object <-> string serialization/deserialization +// JSON serialization/deserialization RCT_EXTERN NSString *RCTJSONStringify(id jsonObject, NSError **error); RCT_EXTERN id RCTJSONParse(NSString *jsonString, NSError **error); RCT_EXTERN id RCTJSONParseMutable(NSString *jsonString, NSError **error); @@ -24,7 +24,7 @@ RCT_EXTERN id RCTJSONParseWithOptions(NSString *jsonString, NSError **error, NSJ // Strip non JSON-safe values from an object graph RCT_EXTERN id RCTJSONClean(id object); -// Get MD5 hash of a string (TODO: currently unused. Remove?) +// Get MD5 hash of a string RCT_EXTERN NSString *RCTMD5Hash(NSString *string); // Get screen metrics in a thread-safe way @@ -64,3 +64,6 @@ RCT_EXTERN id RCTNullIfNil(id value); // Convert data to a Base64-encoded data URL RCT_EXTERN NSURL *RCTDataURL(NSString *mimeType, NSData *data); + +// Gzip functionality - compression level in range 0 - 1 (-1 for default) +RCT_EXTERN NSData *RCTGzipData(NSData *data, float level); diff --git a/React/Base/RCTUtils.m b/React/Base/RCTUtils.m index 7fc620bc0..d342c3be4 100644 --- a/React/Base/RCTUtils.m +++ b/React/Base/RCTUtils.m @@ -10,12 +10,15 @@ #import "RCTUtils.h" #import -#import +#import #import #import +#import +#import + #import "RCTLog.h" NSString *RCTJSONStringify(id jsonObject, NSError **error) @@ -305,3 +308,51 @@ NSURL *RCTDataURL(NSString *mimeType, NSData *data) [data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]]]; } +static BOOL RCTIsGzippedData(NSData *data) +{ + UInt8 *bytes = (UInt8 *)data.bytes; + return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b); +} + +NSData *RCTGzipData(NSData *input, float level) +{ + if (input.length == 0 || RCTIsGzippedData(input)) { + return input; + } + + void *libz = dlopen("libz.dylib", RTLD_NOW); + int (*deflateInit2_)(z_streamp, int, int, int, int, int, const char *, int) = dlsym(libz, "deflateInit2_"); + int (*deflate)(z_streamp, int) = dlsym(libz, "deflate"); + int (*deflateEnd)(z_streamp) = dlsym(libz, "deflateEnd"); + + z_stream stream; + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = (uint)input.length; + stream.next_in = (Bytef *)input.bytes; + stream.total_out = 0; + stream.avail_out = 0; + + static const NSUInteger RCTGZipChunkSize = 16384; + + NSMutableData *output = nil; + int compression = (level < 0.0f)? Z_DEFAULT_COMPRESSION: (int)(roundf(level * 9)); + if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) { + output = [NSMutableData dataWithLength:RCTGZipChunkSize]; + while (stream.avail_out == 0) { + if (stream.total_out >= output.length) { + output.length += RCTGZipChunkSize; + } + stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out; + stream.avail_out = (uInt)(output.length - stream.total_out); + deflate(&stream, Z_FINISH); + } + deflateEnd(&stream); + output.length = stream.total_out; + } + + dlclose(libz); + + return output; +} diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 0b3c2c043..724ace6e6 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -32,8 +32,15 @@ #import "RCTViewNodeProtocol.h" #import "UIView+React.h" -static void RCTTraverseViewNodes(id view, void (^block)(id)); -static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps); +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); + } +} @interface RCTAnimation : NSObject @@ -460,24 +467,6 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) [rootShadowView collectRootUpdatedFrames:viewsWithNewFrames parentConstraint:(CGSize){CSS_UNDEFINED, CSS_UNDEFINED}]; - NSSet *originalViewsWithNewFrames = [viewsWithNewFrames copy]; - NSMutableArray *viewsToCheck = [viewsWithNewFrames.allObjects mutableCopy]; - while (viewsToCheck.count > 0) { - // Better to remove from the front and append to the end - // because of how NSMutableArray is implemented. - // (It's a "round" buffer with stored size and offset.) - - RCTShadowView *viewToCheck = viewsToCheck.firstObject; - [viewsToCheck removeObjectAtIndex:0]; - - if (viewToCheck.layoutOnly) { - [viewsWithNewFrames removeObject:viewToCheck]; - [viewsToCheck addObjectsFromArray:[viewToCheck reactSubviews]]; - } else { - [viewsWithNewFrames addObject:viewToCheck]; - } - } - // Parallel arrays are built and then handed off to main thread NSMutableArray *frameReactTags = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; NSMutableArray *frames = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; @@ -486,30 +475,26 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) NSMutableArray *onLayoutEvents = [NSMutableArray arrayWithCapacity:viewsWithNewFrames.count]; for (RCTShadowView *shadowView in viewsWithNewFrames) { - CGRect frame = shadowView.adjustedFrame; - NSNumber *reactTag = shadowView.reactTag; - [frameReactTags addObject:reactTag]; - [frames addObject:[NSValue valueWithCGRect:frame]]; + [frameReactTags addObject:shadowView.reactTag]; + [frames addObject:[NSValue valueWithCGRect:shadowView.frame]]; [areNew addObject:@(shadowView.isNewView)]; - - RCTShadowView *superview = shadowView; - BOOL parentIsNew = NO; - while (YES) { - superview = superview.superview; - parentIsNew = superview.isNewView; - if (!superview.layoutOnly) { - break; - } + [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), + }, + }; } - [parentsAreNew addObject:@(parentIsNew)]; - - id event = shadowView.hasOnLayout - ? RCTShadowViewOnLayoutEventPayload(shadowView.reactTag, frame) - : (id)kCFNull; [onLayoutEvents addObject:event]; } - for (RCTShadowView *shadowView in originalViewsWithNewFrames) { + for (RCTShadowView *shadowView in viewsWithNewFrames) { // We have to do this after we build the parentsAreNew array. shadowView.newView = NO; } @@ -526,28 +511,24 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) } // Perform layout (possibly animated) - return ^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTResponseSenderBlock callback = uiManager->_layoutAnimation.callback; + return ^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + RCTResponseSenderBlock callback = self->_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 : uiManager->_layoutAnimation.updateAnimation; + RCTAnimation *updateAnimation = isNew ? nil : _layoutAnimation.updateAnimation; BOOL shouldAnimateCreation = isNew && ![parentsAreNew[ii] boolValue]; - RCTAnimation *createAnimation = shouldAnimateCreation ? uiManager->_layoutAnimation.createAnimation : nil; + RCTAnimation *createAnimation = shouldAnimateCreation ? _layoutAnimation.createAnimation : nil; void (^completion)(BOOL) = ^(BOOL finished) { completionsCalled++; if (event != (id)kCFNull) { - [uiManager.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event]; + [self.bridge.eventDispatcher sendInputEventWithName:@"topLayout" body:event]; } if (callback && completionsCalled == frames.count - 1) { callback(@[@(finished)]); @@ -559,13 +540,13 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) [updateAnimation performAnimations:^{ [view reactSetFrame:frame]; for (RCTViewManagerUIBlock block in updateBlocks) { - block(uiManager, viewRegistry); + block(self, _viewRegistry); } } withCompletionBlock:completion]; } else { [view reactSetFrame:frame]; for (RCTViewManagerUIBlock block in updateBlocks) { - block(uiManager, viewRegistry); + block(self, _viewRegistry); } completion(YES); } @@ -587,7 +568,7 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass) createAnimation.property); } for (RCTViewManagerUIBlock block in updateBlocks) { - block(uiManager, viewRegistry); + block(self, _viewRegistry); } } withCompletionBlock:nil]; } @@ -710,135 +691,6 @@ RCT_EXPORT_METHOD(replaceExistingNonRootView:(NSNumber *)reactTag withView:(NSNu removeAtIndices:removeAtIndices]; } -/** - * This method modifies the indices received in manageChildren() to take into - * account views that are layout only. For example, if JS tells native to insert - * view with tag 12 at index 4, but view 12 is layout only, we would want to - * insert its children's tags, tags 13 and 14, at indices 4 and 5 instead. This - * causes us to have to shift the remaining indices to account for the new - * views. - */ -- (void)modifyManageChildren:(NSNumber *)containerReactTag - addChildReactTags:(NSMutableArray *)mutableAddChildReactTags - addAtIndices:(NSMutableArray *)mutableAddAtIndices - removeAtIndices:(NSMutableArray *)mutableRemoveAtIndices -{ - NSUInteger i; - NSMutableArray *containerSubviews = [[_shadowViewRegistry[containerReactTag] reactSubviews] mutableCopy]; - - i = 0; - while (i < containerSubviews.count) { - RCTShadowView *shadowView = containerSubviews[i]; - if (!shadowView.layoutOnly) { - i++; - continue; - } - - [containerSubviews removeObjectAtIndex:i]; - - NSArray *subviews = [shadowView reactSubviews]; - NSUInteger subviewsCount = subviews.count; - NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)]; - [containerSubviews insertObjects:subviews atIndexes:insertionIndexes]; - - NSUInteger removalIndex = [mutableRemoveAtIndices indexOfObject:@(i)]; - if (removalIndex != NSNotFound) { - [mutableRemoveAtIndices removeObjectAtIndex:removalIndex]; - } - - if (subviewsCount != 1) { - for (NSUInteger j = 0, count = mutableRemoveAtIndices.count; j < count; j++) { - NSUInteger atIndex = [mutableRemoveAtIndices[j] unsignedIntegerValue]; - if (atIndex > i) { - mutableRemoveAtIndices[j] = @(atIndex + subviewsCount - 1); - } - } - } - - if (removalIndex != NSNotFound) { - for (NSUInteger j = 0; j < subviewsCount; j++) { - [mutableRemoveAtIndices insertObject:@(i + j) atIndex:removalIndex + j]; - } - } - - if (removalIndex == NSNotFound && subviewsCount != 1) { - for (NSUInteger j = 0, count = mutableAddAtIndices.count; j < count; j++) { - NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue]; - if (atIndex > i) { - mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1); - } - } - } - } - - i = 0; - while (i < mutableAddChildReactTags.count) { - NSNumber *tag = mutableAddChildReactTags[i]; - NSNumber *index = mutableAddAtIndices[i]; - - RCTShadowView *shadowView = _shadowViewRegistry[tag]; - if (!shadowView.layoutOnly) { - i++; - continue; - } - - NSArray *subviews = [shadowView reactSubviews]; - NSUInteger subviewsCount = subviews.count; - [mutableAddAtIndices removeObjectAtIndex:i]; - [mutableAddChildReactTags removeObjectAtIndex:i]; - - for (NSUInteger j = 0; j < subviewsCount; j++) { - [mutableAddChildReactTags insertObject:[subviews[j] reactTag] atIndex:i + j]; - [mutableAddAtIndices insertObject:@(index.unsignedIntegerValue + j) atIndex:i + j]; - } - - for (NSUInteger j = i + subviewsCount, count = mutableAddAtIndices.count; j < count; j++) { - NSUInteger atIndex = [mutableAddAtIndices[j] unsignedIntegerValue]; - mutableAddAtIndices[j] = @(atIndex + subviewsCount - 1); - } - } -} - -- (NSNumber *)containerReactTag:(NSNumber *)containerReactTag offset:(inout NSUInteger *)offset -{ - RCTShadowView *container = _shadowViewRegistry[containerReactTag]; - NSNumber *containerSuperviewReactTag = containerReactTag; - RCTShadowView *superview = container; - - while (superview.layoutOnly) { - RCTShadowView *superviewSuperview = superview.superview; - containerSuperviewReactTag = superviewSuperview.reactTag; - NSMutableArray *reactSubviews = [[superviewSuperview reactSubviews] mutableCopy]; - NSUInteger superviewIndex = [reactSubviews indexOfObject:superview]; - - NSUInteger i = 0; - while (i < superviewIndex) { - RCTShadowView *child = reactSubviews[i]; - if (!child.layoutOnly) { - if (offset) { - (*offset)++; - } - - i++; - continue; - } - - [reactSubviews removeObjectAtIndex:i]; - - NSArray *subviews = [child reactSubviews]; - NSUInteger subviewsCount = subviews.count; - NSIndexSet *insertionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(i, subviewsCount)]; - [reactSubviews insertObjects:subviews atIndexes:insertionIndexes]; - - superviewIndex += subviewsCount - 1; - } - - superview = superviewSuperview; - } - - return containerSuperviewReactTag; -} - RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag moveFromIndices:(NSArray *)moveFromIndices moveToIndices:(NSArray *)moveToIndices @@ -846,109 +698,62 @@ 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]; - 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]; - }]; - } + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ + [uiManager _manageChildren:containerReactTag + moveFromIndices:moveFromIndices + moveToIndices:moveToIndices + addChildReactTags:addChildReactTags + addAtIndices:addAtIndices + removeAtIndices:removeAtIndices + 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(addChildReactTags.count == addAtIndices.count, @"Invalid arguments: addChildReactTags.count == addAtIndices.count"); + 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"); - // Removes are using "before" indices - NSArray *removedChildren = [self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; - [self _removeChildren:removedChildren fromContainer:container]; + // 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]; - 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 - NSMutableDictionary *childrenToAdd = [NSMutableDictionary dictionary]; - for (NSInteger index = 0, count = addAtIndices.count; index < count; index++) { - id view = registry[addChildReactTags[index]]; + // 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]]; if (view) { - childrenToAdd[addAtIndices[index]] = view; + destinationsToChildrenToAdd[addAtIndices[index]] = view; } } - NSArray *sortedIndices = [[childrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)]; + NSArray *sortedIndices = [[destinationsToChildrenToAdd allKeys] sortedArrayUsingSelector:@selector(compare:)]; for (NSNumber *reactIndex in sortedIndices) { - [container insertReactSubview:childrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; + [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } } @@ -1031,72 +836,45 @@ 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; - 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]; - }]; - } -} + // 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; -- (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; - } + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry){ + RCTAssertMainThread(); - // 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]; - } + UIView *view = [manager view]; + if (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); + // 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]; + } - if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { - [_bridgeTransactionListeners addObject:view]; - } - _viewRegistry[reactTag] = 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, uiManager->_defaultViews[viewName], manager); - 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), - }, - }; + if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) { + [uiManager->_bridgeTransactionListeners addObject:view]; + } + } + viewRegistry[reactTag] = view; + }]; } // TODO: remove viewName param as it isn't needed @@ -1110,100 +888,10 @@ RCT_EXPORT_METHOD(updateView:(NSNumber *)reactTag RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; RCTSetShadowViewProps(props, shadowView, _defaultShadowViews[viewName], viewManager); - const BOOL wasLayoutOnly = shadowView.layoutOnly; - NSDictionary *newProps = RCTPropsMerge(shadowView.allProps, props); - shadowView.allProps = newProps; - - const BOOL isLayoutOnly = shadowView.layoutOnly; - - if (wasLayoutOnly != isLayoutOnly) { - // Add/remove node - - if (isLayoutOnly) { - [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTAssertMainThread(); - - UIView *container = viewRegistry[reactTag]; - - const CGRect containerFrame = container.frame; - const CGFloat deltaX = containerFrame.origin.x; - const CGFloat deltaY = containerFrame.origin.y; - - NSUInteger offset = [container.superview.subviews indexOfObject:container]; - [container.subviews enumerateObjectsUsingBlock:^(UIView *subview, NSUInteger idx, __unused BOOL *stop) { - [container removeReactSubview:subview]; - - CGRect subviewFrame = subview.frame; - subviewFrame.origin.x += deltaX; - subviewFrame.origin.y += deltaY; - subview.frame = subviewFrame; - - [container.superview insertReactSubview:subview atIndex:idx + offset]; - }]; - - [container.superview removeReactSubview:container]; - if ([container conformsToProtocol:@protocol(RCTInvalidating)]) { - [(id)container invalidate]; - } - - viewRegistry[reactTag] = nil; - }]; - } else { - NSMutableArray *mutableAddChildReactTags = [[[shadowView reactSubviews] valueForKey:@"reactTag"] mutableCopy]; - NSMutableArray *mutableAddAtIndices = [NSMutableArray arrayWithCapacity:mutableAddChildReactTags.count]; - for (NSUInteger i = 0, count = mutableAddChildReactTags.count; i < count; i++) { - [mutableAddAtIndices addObject:@(i)]; - } - - [self modifyManageChildren:reactTag - addChildReactTags:mutableAddChildReactTags - addAtIndices:mutableAddAtIndices - removeAtIndices:nil]; - - NSUInteger offset = [shadowView.superview.reactSubviews indexOfObject:shadowView]; - NSNumber *containerSuperviewReactTag = [self containerReactTag:shadowView.superview.reactTag offset:&offset]; - UIColor *backgroundColor = shadowView.backgroundColor; - - CGRect shadowViewFrame = shadowView.adjustedFrame; - NSMutableDictionary *newFrames = [NSMutableDictionary dictionaryWithCapacity:mutableAddChildReactTags.count]; - for (NSNumber *childTag in mutableAddChildReactTags) { - RCTShadowView *child = _shadowViewRegistry[childTag]; - newFrames[childTag] = [NSValue valueWithCGRect:child.adjustedFrame]; - } - - [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - RCTAssertMainThread(); - - UIView *containerSuperview = viewRegistry[containerSuperviewReactTag]; - UIView *container = [uiManager createView:reactTag viewName:viewName props:newProps withManager:viewManager backgroundColor:backgroundColor]; - - [containerSuperview insertReactSubview:container atIndex:offset]; - if (RCTRectIsDefined(shadowViewFrame)) { - container.frame = shadowViewFrame; - } - - for (NSUInteger i = 0, count = mutableAddAtIndices.count; i < count; i++) { - NSNumber *tag = mutableAddChildReactTags[i]; - UIView *subview = viewRegistry[tag]; - [containerSuperview removeReactSubview:subview]; - - NSUInteger atIndex = [mutableAddAtIndices[i] unsignedIntegerValue]; - [container insertReactSubview:subview atIndex:atIndex]; - - CGRect subviewFrame = [newFrames[tag] CGRectValue]; - if (RCTRectIsDefined(subviewFrame)) { - subview.frame = subviewFrame; - } - } - }]; - } - } else if (!isLayoutOnly) { - // Update node - [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - UIView *view = viewRegistry[reactTag]; - RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager); - }]; - } + [self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + UIView *view = viewRegistry[reactTag]; + RCTSetViewProps(props, view, uiManager->_defaultViews[viewName], viewManager); + }]; } RCT_EXPORT_METHOD(focus:(NSNumber *)reactTag) @@ -1541,16 +1229,12 @@ RCT_EXPORT_METHOD(zoomToRect:(NSNumber *)reactTag RCT_EXPORT_METHOD(setJSResponder:(NSNumber *)reactTag blockNativeResponder:(__unused BOOL)blockNativeResponder) { - RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; - if (!shadowView) { - RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag); - } else if (shadowView.layoutOnly) { - RCTLogError(@"Cannot set JS responder to layout-only view with tag %@ - %@, %@", reactTag, shadowView, shadowView.allProps); - } else { - [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { - _jsResponder = viewRegistry[reactTag]; - }]; - } + [self addUIBlock:^(__unused RCTUIManager *uiManager, RCTSparseArray *viewRegistry) { + _jsResponder = viewRegistry[reactTag]; + if (!_jsResponder) { + RCTLogError(@"Invalid view set to be the JS responder - tag %zd", reactTag); + } + }]; } RCT_EXPORT_METHOD(clearJSResponder) @@ -1806,27 +1490,3 @@ 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/RCTNavItem.h b/React/Views/RCTNavItem.h index 16c1e7f5d..9b75673c3 100644 --- a/React/Views/RCTNavItem.h +++ b/React/Views/RCTNavItem.h @@ -19,6 +19,7 @@ @property (nonatomic, strong) UIImage *backButtonIcon; @property (nonatomic, copy) NSString *backButtonTitle; @property (nonatomic, assign) BOOL navigationBarHidden; +@property (nonatomic, assign) BOOL shadowHidden; @property (nonatomic, strong) UIColor *tintColor; @property (nonatomic, strong) UIColor *barTintColor; @property (nonatomic, strong) UIColor *titleTextColor; diff --git a/React/Views/RCTNavItemManager.m b/React/Views/RCTNavItemManager.m index dab6ee049..8350af2e9 100644 --- a/React/Views/RCTNavItemManager.m +++ b/React/Views/RCTNavItemManager.m @@ -22,6 +22,7 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(navigationBarHidden, BOOL) +RCT_EXPORT_VIEW_PROPERTY(shadowHidden, BOOL) RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(barTintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 38edc6e50..1c44033f6 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -41,12 +41,6 @@ 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 e5cccbd3f..9d56bb906 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -367,10 +367,8 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st - (NSString *)description { NSString *description = super.description; - if (self.layoutOnly) { - description = [@"* " stringByAppendingString:description]; - } - return [[description substringToIndex:description.length - 1] stringByAppendingFormat:@"; viewName: %@; reactTag: %@; frame: %@>", self.viewName, self.reactTag, NSStringFromCGRect(self.frame)]; + description = [[description substringToIndex:description.length - 1] stringByAppendingFormat:@"; viewName: %@; reactTag: %@; frame: %@>", self.viewName, self.reactTag, NSStringFromCGRect(self.frame)]; + return description; } - (void)addRecursiveDescriptionToString:(NSMutableString *)string atLevel:(NSUInteger)level @@ -394,94 +392,6 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st return description; } -- (BOOL)isLayoutOnly -{ - if (![self.viewName isEqualToString:@"RCTView"]) { - // For now, only `RCTView`s can be layout-only. - return NO; - } - - // dispatch_once is unnecessary because this property SHOULD only be accessed - // on the shadow queue - static NSSet *layoutKeys; - static NSSet *specialKeys; - if (!layoutKeys || !specialKeys) { - // Taken from LayoutPropTypes.js with the exception that borderWidth, - // borderTopWidth, borderBottomWidth, borderLeftWidth, and borderRightWidth - // were removed because black color is assumed - static NSString *const layoutKeyStrings[] = { - @"width", - @"height", - @"top", - @"left", - @"right", - @"bottom", - @"margin", - @"marginVertical", - @"marginHorizontal", - @"marginTop", - @"marginBottom", - @"marginLeft", - @"marginRight", - @"padding", - @"paddingVertical", - @"paddingHorizontal", - @"paddingTop", - @"paddingBottom", - @"paddingLeft", - @"paddingRight", - @"position", - @"flexDirection", - @"flexWrap", - @"justifyContent", - @"alignItems", - @"alignSelf", - @"flex", - }; - layoutKeys = [NSSet setWithObjects:layoutKeyStrings count:sizeof(layoutKeyStrings)/sizeof(*layoutKeyStrings)]; - // layoutKeys are the only keys whose presence does not reject layout-only status. - - static NSString *const specialKeyStrings[] = { - @"accessible", - @"collapsible", - }; - specialKeys = [NSSet setWithObjects:specialKeyStrings count:sizeof(specialKeyStrings)/sizeof(*specialKeyStrings)]; - // specialKeys are keys whose presence does not indicate whether layout-only or not - // their values must be tested below - } - - NSNumber *collapsible = self.allProps[@"collapsible"]; - if (collapsible && !collapsible.boolValue) { - return NO; - } - - NSNumber *accessible = self.allProps[@"accessible"]; - if (accessible && accessible.boolValue) { - return NO; - } - - for (NSString *key in self.allProps) { - if (![specialKeys containsObject:key] && ![layoutKeys containsObject:key]) { - return NO; - } - } - - return YES; -} - -- (CGRect)adjustedFrame -{ - CGRect frame = self.frame; - RCTShadowView *superview = self; - while ((superview = superview.superview) && superview.layoutOnly) { - const CGRect superviewFrame = superview.frame; - frame.origin.x += superviewFrame.origin.x; - frame.origin.y += superviewFrame.origin.y; - } - - return frame; -} - // Margin #define RCT_MARGIN_PROPERTY(prop, metaProp) \ diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 9be9087f6..1c6c2f17d 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -102,8 +102,13 @@ RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor); RCT_REMAP_VIEW_PROPERTY(shadowOffset, layer.shadowOffset, CGSize); RCT_REMAP_VIEW_PROPERTY(shadowOpacity, layer.shadowOpacity, float) RCT_REMAP_VIEW_PROPERTY(shadowRadius, layer.shadowRadius, CGFloat) -RCT_REMAP_VIEW_PROPERTY(transformMatrix, layer.transform, CATransform3D) RCT_REMAP_VIEW_PROPERTY(overflow, clipsToBounds, css_clip_t) +RCT_CUSTOM_VIEW_PROPERTY(transformMatrix, CATransform3D, RCTView) +{ + view.layer.transform = json ? [RCTConvert CATransform3D:json] : defaultView.layer.transform; + // TODO: Improve this by enabling edge antialiasing only for transforms with rotation or skewing + view.layer.allowsEdgeAntialiasing = !CATransform3DIsIdentity(view.layer.transform); +} RCT_CUSTOM_VIEW_PROPERTY(pointerEvents, RCTPointerEvents, RCTView) { if ([view respondsToSelector:@selector(setPointerEvents:)]) { diff --git a/React/Views/RCTViewNodeProtocol.h b/React/Views/RCTViewNodeProtocol.h index 96eb78f1a..e78cc2ce7 100644 --- a/React/Views/RCTViewNodeProtocol.h +++ b/React/Views/RCTViewNodeProtocol.h @@ -15,11 +15,10 @@ @protocol RCTViewNodeProtocol @property (nonatomic, copy) NSNumber *reactTag; -@property (nonatomic, assign) CGRect frame; - (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex; - (void)removeReactSubview:(id)subview; -- (NSArray *)reactSubviews; +- (NSMutableArray *)reactSubviews; - (id)reactSuperview; - (NSNumber *)reactTagAtPoint:(CGPoint)point; diff --git a/React/Views/RCTWrapperViewController.m b/React/Views/RCTWrapperViewController.m index c1893353b..b9fd8ec3d 100644 --- a/React/Views/RCTWrapperViewController.m +++ b/React/Views/RCTWrapperViewController.m @@ -63,6 +63,20 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) _currentBottomLayoutGuide = self.bottomLayoutGuide; } +static UIView *RCTFindNavBarShadowViewInView(UIView *view) +{ + if ([view isKindOfClass:[UIImageView class]] && view.bounds.size.height <= 1) { + return view; + } + for (UIView *subview in view.subviews) { + UIView *shadowView = RCTFindNavBarShadowViewInView(subview); + if (shadowView) { + return shadowView; + } + } + return nil; +} + - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; @@ -71,20 +85,18 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) if ([self.parentViewController isKindOfClass:[UINavigationController class]]) { [self.navigationController - setNavigationBarHidden:_navItem.navigationBarHidden - animated:animated]; - - if (!_navItem) { - return; - } + setNavigationBarHidden:_navItem.navigationBarHidden + animated:animated]; UINavigationBar *bar = self.navigationController.navigationBar; bar.barTintColor = _navItem.barTintColor; bar.tintColor = _navItem.tintColor; bar.translucent = _navItem.translucent; - if (_navItem.titleTextColor) { - [bar setTitleTextAttributes:@{NSForegroundColorAttributeName : _navItem.titleTextColor}]; - } + bar.titleTextAttributes = _navItem.titleTextColor ? @{ + NSForegroundColorAttributeName: _navItem.titleTextColor + } : nil; + + RCTFindNavBarShadowViewInView(bar).hidden = _navItem.shadowHidden; UINavigationItem *item = self.navigationItem; item.title = _navItem.title; @@ -129,7 +141,8 @@ RCT_NOT_IMPLEMENTED(-initWithCoder:(NSCoder *)aDecoder) // finishes, be it a swipe to go back or a standard tap on the back button [super didMoveToParentViewController:parent]; if (parent == nil || [parent isKindOfClass:[UINavigationController class]]) { - [self.navigationListener wrapperViewController:self didMoveToNavigationController:(UINavigationController *)parent]; + [self.navigationListener wrapperViewController:self + didMoveToNavigationController:(UINavigationController *)parent]; } } diff --git a/package.json b/package.json index 39f03c656..04bb0a588 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "init.sh", "LICENSE", "PATENTS", - "README.md" + "README.md", + "jestSupport" ], "scripts": { "test": "jest", diff --git a/packager/debugger.html b/packager/debugger.html index 0d0084559..c9a816023 100644 --- a/packager/debugger.html +++ b/packager/debugger.html @@ -27,6 +27,11 @@ window.onbeforeunload = function() { } }; +// Alias native implementations needed by the debugger before platform-specific +// implementations are loaded into the global namespace +var debuggerSetTimeout = window.setTimeout; +var DebuggerWebSocket = window.WebSocket; + function setStatus(status) { document.getElementById('status').innerHTML = status; } @@ -58,35 +63,43 @@ var messageHandlers = { sendReply(JSON.stringify(returnValue)); } } +}; + +function connectToDebuggerProxy() { + var ws = new DebuggerWebSocket('ws://' + window.location.host + '/debugger-proxy'); + + ws.onopen = function() { + if (sessionID) { + setStatus('Debugger session #' + sessionID + ' active'); + ws.send(JSON.stringify({replyID: parseInt(sessionID, 10)})); + } else { + setStatus('Waiting, press ⌘R in simulator to reload and connect'); + } + }; + + ws.onmessage = function(message) { + var object = JSON.parse(message.data); + var sendReply = function(result) { + ws.send(JSON.stringify({replyID: object.id, result: result})); + }; + var handler = messageHandlers[object.method]; + if (handler) { + handler(object, sendReply); + } else { + console.warn('Unknown method: ' + object.method); + } + }; + + ws.onclose = function() { + setStatus('Disconnected from proxy. Attempting reconnection. Is node server running?'); + + sessionID = null; + window.localStorage.removeItem('sessionID'); + debuggerSetTimeout(connectToDebuggerProxy, 100); + }; } -var ws = new WebSocket('ws://' + window.location.host + '/debugger-proxy'); - -ws.onopen = function() { - if (sessionID) { - setStatus('Debugger session #' + sessionID + ' active'); - ws.send(JSON.stringify({replyID: parseInt(sessionID, 10)})); - } else { - setStatus('Waiting, press ⌘R in simulator to reload and connect'); - } -} - -ws.onmessage = function(message) { - var object = JSON.parse(message.data); - var sendReply = function(result) { - ws.send(JSON.stringify({replyID: object.id, result: result})); - } - var handler = messageHandlers[object.method]; - if (handler) { - handler(object, sendReply); - } else { - console.warn('Unknown method: ' + object.method); - } -} - -ws.onclose = function() { - setStatus('Disconnected from proxy. Is node server running?'); -} +connectToDebuggerProxy(); function loadScript(src, callback) { var script = document.createElement('script');