Skip to content

Commit

Permalink
[ASDisplayNode] Fix an issue that causes a node to sometimes return …
Browse files Browse the repository at this point in the history
…an outdated calculated size or size range (#808)
  • Loading branch information
nguyenhuy authored May 21, 2018
1 parent 06358d8 commit b9ae6c4
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- Prevent UITextView from updating contentOffset while deallocating [Michael Schneider](https://github.com/maicki)
- [ASCollectionNode/ASTableNode] Fix a crash occurs while remeasuring cell nodes. [Huy Nguyen](https://github.com/nguyenhuy) [#917](https://github.com/TextureGroup/Texture/pull/917)
- Fix an issue where ASConfigurationDelegate would not call out for "control" users. If set, it now receives events whenever an experimental feature decision point occurs, whether it's enabled or not. [Adlai Holler](https://github.com/Adlai-Holler)
- [ASDisplayNode] Fix an issue that causes a node to sometimes return an outdated calculated size or size range. [Huy Nguyen](https://github.com/nguyenhuy) [#808](https://github.com/TextureGroup/Texture/pull/808)

## 2.6
- [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon)
Expand Down
28 changes: 13 additions & 15 deletions Source/ASDisplayNode+Layout.mm
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ - (ASLayout *)calculatedLayout
- (CGSize)calculatedSize
{
ASDN::MutexLocker l(__instanceLock__);
if (_pendingDisplayNodeLayout != nullptr) {
if (_pendingDisplayNodeLayout != nullptr && _pendingDisplayNodeLayout->isValid(_layoutVersion)) {
return _pendingDisplayNodeLayout->layout.size;
}
return _calculatedDisplayNodeLayout->layout.size;
Expand All @@ -184,7 +184,7 @@ - (ASSizeRange)constrainedSizeForCalculatedLayout

- (ASSizeRange)_locked_constrainedSizeForCalculatedLayout
{
if (_pendingDisplayNodeLayout != nullptr) {
if (_pendingDisplayNodeLayout != nullptr && _pendingDisplayNodeLayout->isValid(_layoutVersion)) {
return _pendingDisplayNodeLayout->constrainedSize;
}
return _calculatedDisplayNodeLayout->constrainedSize;
Expand Down Expand Up @@ -304,24 +304,22 @@ - (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds
}

CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size);
NSUInteger calculatedVersion = _calculatedDisplayNodeLayout->version;

// Prefer a newer and not yet applied _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout
// If there is no such _pending, check if _calculated is valid to reuse (avoiding recalculation below).
BOOL pendingLayoutIsPreferred = NO;
if (_pendingDisplayNodeLayout != nullptr) {
if (_pendingDisplayNodeLayout != nullptr && _pendingDisplayNodeLayout->isValid(_layoutVersion)) {
NSUInteger calculatedVersion = _calculatedDisplayNodeLayout->version;
NSUInteger pendingVersion = _pendingDisplayNodeLayout->version;
if (pendingVersion >= _layoutVersion) {
if (pendingVersion > calculatedVersion) {
pendingLayoutIsPreferred = YES; // Newer _pending
} else if (pendingVersion == calculatedVersion
&& !ASSizeRangeEqualToSizeRange(_pendingDisplayNodeLayout->constrainedSize,
_calculatedDisplayNodeLayout->constrainedSize)) {
pendingLayoutIsPreferred = YES; // _pending with a different constrained size
}
}
}
BOOL calculatedLayoutIsReusable = (calculatedVersion >= _layoutVersion
if (pendingVersion > calculatedVersion) {
pendingLayoutIsPreferred = YES; // Newer _pending
} else if (pendingVersion == calculatedVersion
&& !ASSizeRangeEqualToSizeRange(_pendingDisplayNodeLayout->constrainedSize,
_calculatedDisplayNodeLayout->constrainedSize)) {
pendingLayoutIsPreferred = YES; // _pending with a different constrained size
}
}
BOOL calculatedLayoutIsReusable = (_calculatedDisplayNodeLayout->isValid(_layoutVersion)
&& (_calculatedDisplayNodeLayout->requestedLayoutFromAbove
|| CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout)));
if (!pendingLayoutIsPreferred && calculatedLayoutIsReusable) {
Expand Down
7 changes: 6 additions & 1 deletion Source/Private/ASDisplayNodeLayout.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ struct ASDisplayNodeLayout {
*/
ASDisplayNodeLayout()
: layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), requestedLayoutFromAbove(NO), version(0) {};


/**
* Returns whether this is valid for a given version
*/
BOOL isValid(NSUInteger version);

/**
* Returns whether this is valid for a given constrained size, parent size, and version
*/
Expand Down
8 changes: 6 additions & 2 deletions Source/Private/ASDisplayNodeLayout.mm
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@

#import <AsyncDisplayKit/ASDisplayNodeLayout.h>

BOOL ASDisplayNodeLayout::isValid(NSUInteger versionArg)
{
return layout != nil && version >= versionArg;
}

BOOL ASDisplayNodeLayout::isValid(ASSizeRange theConstrainedSize, CGSize theParentSize, NSUInteger versionArg)
{
return version >= versionArg
&& layout != nil
return isValid(versionArg)
&& CGSizeEqualToSize(parentSize, theParentSize)
&& ASSizeRangeEqualToSizeRange(constrainedSize, theConstrainedSize);
}
106 changes: 92 additions & 14 deletions Tests/ASLayoutEngineTests.mm
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@ @implementation ASLayoutEngineTests {
ASTLayoutFixture *fixture2;
ASTLayoutFixture *fixture3;
ASTLayoutFixture *fixture4;
ASTLayoutFixture *fixture5;

// fixtures 1 and 3 share the same exact node A layout spec block.
// fixtures 1, 3 and 5 share the same exact node A layout spec block.
// we don't want the infra to call -setNeedsLayout when we switch fixtures
// so we need to use the same exact block.
ASLayoutSpecBlock fixture1and3NodeALayoutSpecBlock;
ASLayoutSpecBlock fixture1and3and5NodeALayoutSpecBlock;

UIWindow *window;
UIViewController *vc;
Expand Down Expand Up @@ -68,11 +69,12 @@ - (void)setUp
ASLayoutSpecBlock b = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
return [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentSpaceBetween alignItems:ASStackLayoutAlignItemsStart children:@[ nodeB, nodeC, nodeD ]];
};
fixture1and3NodeALayoutSpecBlock = b;
fixture1and3and5NodeALayoutSpecBlock = b;
fixture1 = [self createFixture1];
fixture2 = [self createFixture2];
fixture3 = [self createFixture3];
fixture4 = [self createFixture4];
fixture5 = [self createFixture5];

nodeA.frame = vc.view.bounds;
nodeA.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Expand Down Expand Up @@ -181,6 +183,55 @@ - (void)testLayoutTransitionWithAsyncMeasurement
[self verifyFixture:fixture2];
}

/**
* Transition from fixture1 to Fixture2 on node A.
*
* Expect A and D to calculate once on main, and
* to receive calculatedLayoutDidChange on main,
* then to get animateLayoutTransition: and didCompleteLayoutTransition: on main.
*/
- (void)testLayoutTransitionWithSyncMeasurement
{
[self stubCalculatedLayoutDidChange];

// Precondition
XCTAssertFalse(CGSizeEqualToSize(fixture5.layout.size, fixture1.layout.size));

// First, apply fixture 5 and run a measurement pass, but don't run a layout pass
// After this step, nodes will have pending layouts that are not yet applied
[fixture5 apply];
[fixture5 withSizeRangesForAllNodesUsingBlock:^(ASLayoutTestNode * _Nonnull node, ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange])
.onMainThread();
}];

[nodeA layoutThatFits:ASSizeRangeMake(fixture5.layout.size)];

// Assert that node A has layout size and size range from fixture 5
XCTAssertTrue(CGSizeEqualToSize(fixture5.layout.size, nodeA.calculatedSize));
XCTAssertTrue(ASSizeRangeEqualToSizeRange([fixture5 firstSizeRangeForNode:nodeA], nodeA.constrainedSizeForCalculatedLayout));

// Then switch to fixture 1 and kick off a synchronous layout transition
// Unapplied pending layouts from the previous measurement pass will be outdated
[fixture1 apply];
[fixture1 withSizeRangesForAllNodesUsingBlock:^(ASLayoutTestNode * _Nonnull node, ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange])
.onMainThread();
}];

OCMExpect([nodeA.mock animateLayoutTransition:OCMOCK_ANY]).onMainThread();
OCMExpect([nodeA.mock didCompleteLayoutTransition:OCMOCK_ANY]).onMainThread();

[nodeA transitionLayoutWithAnimation:NO shouldMeasureAsync:NO measurementCompletion:nil];

// Assert that node A picks up new layout size and size range from fixture 1
XCTAssertTrue(CGSizeEqualToSize(fixture1.layout.size, nodeA.calculatedSize));
XCTAssertTrue(ASSizeRangeEqualToSizeRange([fixture1 firstSizeRangeForNode:nodeA], nodeA.constrainedSizeForCalculatedLayout));

[window layoutIfNeeded];
[self verifyFixture:fixture1];
}

/**
* Start at fixture 1.
* Trigger an async transition to fixture 2.
Expand Down Expand Up @@ -351,7 +402,7 @@ - (void)stubCalculatedLayoutDidChange
/**
* Fixture 1: A basic horizontal stack, all single-pass.
*
* [A: HorizStack([B, C, D])]. B is (1x1), C is (2x1), D is (1x1)
* [A: HorizStack([B, C, D])]. A is (10x1), B is (1x1), C is (2x1), D is (1x1)
*/
- (ASTLayoutFixture *)createFixture1
{
Expand All @@ -373,14 +424,14 @@ - (ASTLayoutFixture *)createFixture1
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]];
fixture.layout = layoutA;

[fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA];
[fixture.layoutSpecBlocks setObject:fixture1and3and5NodeALayoutSpecBlock forKey:nodeA];
return fixture;
}

/**
* Fixture 2: A simple transition away from fixture 1.
*
* [A: HorizStack([B, C, E])]. B is (1x1), C is (4x1), E is (1x1)
* [A: HorizStack([B, C, E])]. A is (10x1), B is (1x1), C is (4x1), E is (1x1)
*
* From fixture 1:
* B survives with same layout
Expand Down Expand Up @@ -418,7 +469,7 @@ - (ASTLayoutFixture *)createFixture2
/**
* Fixture 3: Multipass stack layout
*
* [A: HorizStack([B, C, D])]. B is (7x1), C is (2x1), D is (1x1)
* [A: HorizStack([B, C, D])]. A is (10x1), B is (7x1), C is (2x1), D is (1x1)
*
* nodeB (which has flexShrink=1) will return 8x1 for its size during the first
* stack pass, and it'll be subject to a second pass where it returns 7x1.
Expand Down Expand Up @@ -446,14 +497,14 @@ - (ASTLayoutFixture *)createFixture3
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{10,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]];
fixture.layout = layoutA;

[fixture.layoutSpecBlocks setObject:fixture1and3NodeALayoutSpecBlock forKey:nodeA];
[fixture.layoutSpecBlocks setObject:fixture1and3and5NodeALayoutSpecBlock forKey:nodeA];
return fixture;
}

/**
* Fixture 4: A different simple transition away from fixture 1.
*
* [A: HorizStack([B, D, E])]. B is (1x1), D is (2x1), E is (1x1)
* [A: HorizStack([B, D, E])]. A is (10x1), B is (1x1), D is (2x1), E is (1x1)
*
* From fixture 1:
* B survives with same layout
Expand Down Expand Up @@ -494,18 +545,45 @@ - (ASTLayoutFixture *)createFixture4
return fixture;
}

/**
* Fixture 5: Same as fixture 1, but with a bigger root node (node A).
*
* [A: HorizStack([B, C, D])]. A is (15x1), B is (1x1), C is (2x1), D is (1x1)
*/
- (ASTLayoutFixture *)createFixture5
{
auto fixture = [[ASTLayoutFixture alloc] init];

// nodeB
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeB];
auto layoutB = [ASLayout layoutWithLayoutElement:nodeB size:{1,1} position:{0,0} sublayouts:nil];

// nodeC
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeC];
auto layoutC = [ASLayout layoutWithLayoutElement:nodeC size:{2,1} position:{4,0} sublayouts:nil];

// nodeD
[fixture addSizeRange:{{0, 0}, {INFINITY, 1}} forNode:nodeD];
auto layoutD = [ASLayout layoutWithLayoutElement:nodeD size:{1,1} position:{9,0} sublayouts:nil];

[fixture addSizeRange:{{15, 1}, {15, 1}} forNode:nodeA];
auto layoutA = [ASLayout layoutWithLayoutElement:nodeA size:{15,1} position:ASPointNull sublayouts:@[ layoutB, layoutC, layoutD ]];
fixture.layout = layoutA;

[fixture.layoutSpecBlocks setObject:fixture1and3and5NodeALayoutSpecBlock forKey:nodeA];
return fixture;
}

- (void)runFirstLayoutPassWithFixture:(ASTLayoutFixture *)fixture
{
[fixture apply];
for (ASLayoutTestNode *node in fixture.allNodes) {
[fixture withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread();
}];
[fixture withSizeRangesForAllNodesUsingBlock:^(ASLayoutTestNode * _Nonnull node, ASSizeRange sizeRange) {
OCMExpect([node.mock calculateLayoutThatFits:sizeRange]).onMainThread();

if (!stubbedCalculatedLayoutDidChange) {
OCMExpect([node.mock calculatedLayoutDidChange]).onMainThread();
}
}
}];

// Trigger CA layout pass.
[window layoutIfNeeded];
Expand Down
3 changes: 3 additions & 0 deletions Tests/ASTLayoutFixture.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ AS_SUBCLASSING_RESTRICTED
/// Get the first expected size range for the node.
- (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node;

/// Enumerate all the size ranges for all the nodes using the provided block.
- (void)withSizeRangesForAllNodesUsingBlock:(void (^)(ASLayoutTestNode *node, ASSizeRange sizeRange))block;

/// Enumerate all the size ranges for the node.
- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange sizeRange))block;

Expand Down
9 changes: 9 additions & 0 deletions Tests/ASTLayoutFixture.mm
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ - (ASSizeRange)firstSizeRangeForNode:(ASLayoutTestNode *)node
return r;
}

- (void)withSizeRangesForAllNodesUsingBlock:(void (^)(ASLayoutTestNode * _Nonnull, ASSizeRange))block
{
for (ASLayoutTestNode *node in self.allNodes) {
[self withSizeRangesForNode:node block:^(ASSizeRange sizeRange) {
block(node, sizeRange);
}];
}
}

- (void)withSizeRangesForNode:(ASLayoutTestNode *)node block:(void (^)(ASSizeRange))block
{
for (NSValue *value in [_sizeRanges objectForKey:node]) {
Expand Down

0 comments on commit b9ae6c4

Please sign in to comment.