diff --git a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m index f2715947a4e..cfe3959bc75 100644 --- a/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m +++ b/Examples/UIExplorer/UIExplorerUnitTests/RCTShadowViewTests.m @@ -17,11 +17,23 @@ #import "RCTShadowView.h" @interface RCTShadowViewTests : XCTestCase - +@property (nonatomic, strong) RCTShadowView *parentView; @end @implementation RCTShadowViewTests +- (void)setUp +{ + [super setUp]; + + self.parentView = [self _shadowViewWithStyle:^(css_style_t *style) { + style->flex_direction = CSS_FLEX_DIRECTION_COLUMN; + style->dimensions[0] = 440; + style->dimensions[1] = 440; + }]; + self.parentView.reactTag = @1; // must be valid rootView tag +} + // Just a basic sanity test to ensure css-layout is applied correctly in the context of our shadow view hierarchy. // // ==================================== @@ -69,33 +81,87 @@ style->flex = 1; }]; - RCTShadowView *parentView = [self _shadowViewWithStyle:^(css_style_t *style) { - style->flex_direction = CSS_FLEX_DIRECTION_COLUMN; - style->padding[0] = 10; - style->padding[1] = 10; - style->padding[2] = 10; - style->padding[3] = 10; - style->dimensions[0] = 440; - style->dimensions[1] = 440; - }]; + self.parentView.cssNode->style.padding[0] = 10; + self.parentView.cssNode->style.padding[1] = 10; + self.parentView.cssNode->style.padding[2] = 10; + self.parentView.cssNode->style.padding[3] = 10; - [parentView insertReactSubview:headerView atIndex:0]; - [parentView insertReactSubview:mainView atIndex:1]; - [parentView insertReactSubview:footerView atIndex:2]; + [self.parentView insertReactSubview:headerView atIndex:0]; + [self.parentView insertReactSubview:mainView atIndex:1]; + [self.parentView insertReactSubview:footerView atIndex:2]; - parentView.reactTag = @1; // must be valid rootView tag - [parentView collectRootUpdatedFrames]; + [self.parentView collectRootUpdatedFrames]; - XCTAssertTrue(CGRectEqualToRect([parentView measureLayoutRelativeToAncestor:parentView], CGRectMake(0, 0, 440, 440))); - XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets([parentView paddingAsInsets], UIEdgeInsetsMake(10, 10, 10, 10))); + XCTAssertTrue(CGRectEqualToRect([self.parentView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(0, 0, 440, 440))); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets([self.parentView paddingAsInsets], UIEdgeInsetsMake(10, 10, 10, 10))); - XCTAssertTrue(CGRectEqualToRect([headerView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 10, 420, 100))); - XCTAssertTrue(CGRectEqualToRect([mainView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 120, 420, 200))); - XCTAssertTrue(CGRectEqualToRect([footerView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 330, 420, 100))); + XCTAssertTrue(CGRectEqualToRect([headerView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 10, 420, 100))); + XCTAssertTrue(CGRectEqualToRect([mainView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 120, 420, 200))); + XCTAssertTrue(CGRectEqualToRect([footerView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 330, 420, 100))); - XCTAssertTrue(CGRectEqualToRect([leftView measureLayoutRelativeToAncestor:parentView], CGRectMake(10, 120, 100, 200))); - XCTAssertTrue(CGRectEqualToRect([centerView measureLayoutRelativeToAncestor:parentView], CGRectMake(120, 120, 200, 200))); - XCTAssertTrue(CGRectEqualToRect([rightView measureLayoutRelativeToAncestor:parentView], CGRectMake(330, 120, 100, 200))); + XCTAssertTrue(CGRectEqualToRect([leftView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(10, 120, 100, 200))); + XCTAssertTrue(CGRectEqualToRect([centerView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(120, 120, 200, 200))); + XCTAssertTrue(CGRectEqualToRect([rightView measureLayoutRelativeToAncestor:self.parentView], CGRectMake(330, 120, 100, 200))); +} + +- (void)testAssignsSuggestedWidthDimension +{ + [self _withShadowViewWithStyle:^(css_style_t *style) { + style->position[CSS_LEFT] = 0; + style->position[CSS_TOP] = 0; + style->dimensions[CSS_HEIGHT] = 10; + } + assertRelativeLayout:CGRectMake(0, 0, 3, 10) + withIntrinsicContentSize:CGSizeMake(3, UIViewNoIntrinsicMetric)]; +} + +- (void)testAssignsSuggestedHeightDimension +{ + [self _withShadowViewWithStyle:^(css_style_t *style) { + style->position[CSS_LEFT] = 0; + style->position[CSS_TOP] = 0; + style->dimensions[CSS_WIDTH] = 10; + } + assertRelativeLayout:CGRectMake(0, 0, 10, 4) + withIntrinsicContentSize:CGSizeMake(UIViewNoIntrinsicMetric, 4)]; +} + +- (void)testDoesNotOverrideDimensionStyleWithSuggestedDimensions +{ + [self _withShadowViewWithStyle:^(css_style_t *style) { + style->position[CSS_LEFT] = 0; + style->position[CSS_TOP] = 0; + style->dimensions[CSS_WIDTH] = 10; + style->dimensions[CSS_HEIGHT] = 10; + } + assertRelativeLayout:CGRectMake(0, 0, 10, 10) + withIntrinsicContentSize:CGSizeMake(3, 4)]; +} + +- (void)testDoesNotAssignSuggestedDimensionsWhenStyledWithFlexAttribute +{ + float parentWidth = self.parentView.cssNode->style.dimensions[CSS_WIDTH]; + float parentHeight = self.parentView.cssNode->style.dimensions[CSS_HEIGHT]; + [self _withShadowViewWithStyle:^(css_style_t *style) { + style->flex = 1; + } + assertRelativeLayout:CGRectMake(0, 0, parentWidth, parentHeight) + withIntrinsicContentSize:CGSizeMake(3, 4)]; +} + +- (void)_withShadowViewWithStyle:(void(^)(css_style_t *style))styleBlock + assertRelativeLayout:(CGRect)expectedRect + withIntrinsicContentSize:(CGSize)contentSize +{ + RCTShadowView *view = [self _shadowViewWithStyle:styleBlock]; + [self.parentView insertReactSubview:view atIndex:0]; + view.intrinsicContentSize = contentSize; + [self.parentView collectRootUpdatedFrames]; + CGRect actualRect = [view measureLayoutRelativeToAncestor:self.parentView]; + XCTAssertTrue(CGRectEqualToRect(expectedRect, actualRect), + @"Expected layout to be %@, got %@", + NSStringFromCGRect(expectedRect), + NSStringFromCGRect(actualRect)); } - (RCTShadowView *)_shadowViewWithStyle:(void(^)(css_style_t *style))styleBlock diff --git a/React/Modules/RCTUIManager.h b/React/Modules/RCTUIManager.h index bdcf2e2c535..8a14a4a02ee 100644 --- a/React/Modules/RCTUIManager.h +++ b/React/Modules/RCTUIManager.h @@ -60,6 +60,12 @@ RCT_EXTERN NSString *const RCTUIManagerRootViewKey; */ - (void)setFrame:(CGRect)frame forView:(UIView *)view; +/** + * Set the natural size of a view, which is used when no explicit size is set. + * Use UIViewNoIntrinsicMetric to ignore a dimension. + */ +- (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view; + /** * Update the background color of a root view. This is usually triggered by * manually setting the background color of the root view with native code. diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index d71939130f3..d0db1c40aa5 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -399,6 +399,21 @@ extern NSString *RCTBridgeModuleNameForClass(Class cls); }); } +- (void)setIntrinsicContentSize:(CGSize)size forView:(UIView *)view +{ + RCTAssertMainThread(); + + NSNumber *reactTag = view.reactTag; + dispatch_async(_shadowQueue, ^{ + RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; + RCTAssert(shadowView != nil, @"Could not locate root view with tag #%@", reactTag); + + shadowView.intrinsicContentSize = size; + + [self batchDidComplete]; + }); +} + - (void)setBackgroundColor:(UIColor *)color forRootView:(UIView *)rootView { RCTAssertMainThread(); diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index 40917304d48..e845060f959 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -68,6 +68,12 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry - (void)setTopLeft:(CGPoint)topLeft; - (void)setSize:(CGSize)size; +/** + * Set the natural size of the view, which is used when no explicit size is set. + * Use UIViewNoIntrinsicMetric to ignore a dimension. + */ +- (void)setIntrinsicContentSize:(CGSize)size; + /** * Size flexibility type used to find size constraints. * Default to RCTRootViewSizeFlexibilityNone diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index 42a2f1b8847..c755bbade04 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -535,6 +535,29 @@ RCT_POSITION_PROPERTY(Left, left, LEFT) [self dirtyLayout]; } +static inline BOOL +RCTAssignSuggestedDimension(css_node_t *css_node, int dimension, CGFloat amount) +{ + if (amount != UIViewNoIntrinsicMetric + && isnan(css_node->style.dimensions[dimension])) { + css_node->style.dimensions[dimension] = amount; + return YES; + } + return NO; +} + +- (void)setIntrinsicContentSize:(CGSize)size +{ + if (_cssNode->style.flex == 0) { + BOOL dirty = NO; + dirty |= RCTAssignSuggestedDimension(_cssNode, CSS_HEIGHT, size.height); + dirty |= RCTAssignSuggestedDimension(_cssNode, CSS_WIDTH, size.width); + if (dirty) { + [self dirtyLayout]; + } + } +} + - (void)setTopLeft:(CGPoint)topLeft { _cssNode->style.position[CSS_LEFT] = topLeft.x; diff --git a/runXcodeTests.sh b/runXcodeTests.sh index ab7ce69c1ac..fda82d67f0b 100755 --- a/runXcodeTests.sh +++ b/runXcodeTests.sh @@ -10,16 +10,9 @@ if [ -z "$1" ] exit 255 fi -xctool \ - -project IntegrationTests/IntegrationTests.xcodeproj \ - -scheme IntegrationTests \ - -sdk iphonesimulator8.1 \ - -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ - build test - xctool \ -project Examples/UIExplorer/UIExplorer.xcodeproj \ -scheme UIExplorer \ - -sdk iphonesimulator8.1 \ + -sdk iphonesimulator${1} \ -destination "platform=iOS Simulator,OS=${1},name=iPhone 5" \ build test