[ASLayout] Revisit the flattening algorithm (#395)

* Implement tests for the layout flattening process

* Refactor the flattening algorithm
- Remove flattened flag
- No more self check
- Stop traversing a layout tree branch when hits a displaynode node.
- Reuse as many existing ASLayout objects as possible

* Update changelog

* Ceil position values before comparing

* Explain why sublayout elements must be retained
This commit is contained in:
Huy Nguyen
2017-06-30 13:22:34 -07:00
committed by Garrett Moon
parent 7dce48174e
commit a07d6bf114
9 changed files with 287 additions and 52 deletions
@@ -421,6 +421,7 @@
DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; };
E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E516FC7D1E9FE24200714FF4 /* ASHashing.h */; };
E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E516FC7E1E9FE24200714FF4 /* ASHashing.m */; };
E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; };
E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; };
E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; };
E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; };
@@ -890,6 +891,7 @@
DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = "<group>"; };
E516FC7D1E9FE24200714FF4 /* ASHashing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = "<group>"; };
E516FC7E1E9FE24200714FF4 /* ASHashing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = "<group>"; };
E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutFlatteningTests.m; sourceTree = "<group>"; };
E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = "<group>"; };
E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = "<group>"; };
E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = "<group>"; };
@@ -1173,6 +1175,7 @@
058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */,
058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.m */,
697B31591CFE4B410049936F /* ASEditableTextNodeTests.m */,
E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */,
052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */,
058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */,
3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */,
@@ -2044,6 +2047,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */,
29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */,
CC051F1F1D7A286A006434CB /* ASCALayerTests.m in Sources */,
242995D31B29743C00090100 /* ASBasicImageDownloaderTests.m in Sources */,
+2
View File
@@ -9,6 +9,8 @@
- [Yoga] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga-powered layout calculation. [Scott Goodson](https://github.com/appleguy)
- Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon)
- Migrated unit tests to OCMock 3.4 (from 2.2) and improved the multiplex image node tests. [Adlai Holler](https://github.com/Adlai-Holler)
- Fix CollectionNode double-load issue. This should significantly improve performance in cases where a collection node has content immediately available on first layout i.e. not fetched from the network. [Adlai Holler](https://github.com/Adlai-Holler)
- Overhaul layout flattening algorithm [Huy Nguyen](https://github.com/nguyenhuy) [#395](https://github.com/TextureGroup/Texture/pull/395).
## 2.3.3
- [ASTextKitFontSizeAdjuster] Replace use of NSAttributedString's boundingRectWithSize:options:context: with NSLayoutManager's boundingRectForGlyphRange:inTextContainer: [Ricky Cancro](https://github.com/rcancro)
+1 -7
View File
@@ -36,10 +36,6 @@
- (ASSizeRange)_locked_constrainedSizeForLayoutPass;
@end
@interface ASLayout (YogaInternal)
@property (nonatomic, getter=isFlattened) BOOL flattened;
@end
@implementation ASDisplayNode (Yoga)
- (void)setYogaChildren:(NSArray *)yogaChildren
@@ -174,9 +170,7 @@
NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:childCount];
for (ASDisplayNode *subnode in self.yogaChildren) {
ASLayout *sublayout = [subnode layoutForYogaNode];
sublayout.flattened = YES;
[sublayouts addObject:sublayout];
[sublayouts addObject:[subnode layoutForYogaNode]];
}
// The layout for self should have position CGPointNull, but include the calculated size.
-8
View File
@@ -134,14 +134,6 @@ ASDISPLAYNODE_EXTERN_C_END
*/
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size AS_WARN_UNUSED_RESULT;
/**
* Convenience initializer that creates a layout based on the values of the given layout, with a new position
*
* @param layout The layout to use to create the new layout
* @param position The position of the new layout
*/
+ (instancetype)layoutWithLayout:(ASLayout *)layout position:(CGPoint)position AS_WARN_UNUSED_RESULT;
/**
* Traverses the existing layout tree and generates a new tree that represents only ASDisplayNode layouts
*/
+63 -36
View File
@@ -37,7 +37,7 @@ extern BOOL ASPointIsNull(CGPoint point)
/**
* Creates an defined number of " |" indent blocks for the recursive description.
*/
static inline NSString * descriptionIndents(NSUInteger indents)
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents)
{
NSMutableString *description = [NSMutableString string];
for (NSUInteger i = 0; i < indents; i++) {
@@ -49,14 +49,31 @@ static inline NSString * descriptionIndents(NSUInteger indents)
return description;
}
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout)
{
return layout.type == ASLayoutElementTypeDisplayNode;
}
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsFlattened(ASLayout *layout)
{
// A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts.
if (! ASPointIsNull(layout.position)) {
return NO;
}
for (ASLayout *sublayout in layout.sublayouts) {
if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout.sublayouts.count > 0) {
return NO;
}
}
return YES;
}
@interface ASLayout () <ASDescriptionProvider>
{
ASLayoutElementType _layoutElementType;
}
/**
* A boolean describing if the current layout has been flattened.
*/
@property (nonatomic, getter=isFlattened) BOOL flattened;
/*
* Caches all sublayouts if set to YES or destroys the sublayout cache if set to NO. Defaults to YES
@@ -117,7 +134,7 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
_size = size;
if (ASPointIsNull(position) == NO) {
_position = CGPointMake(ASCeilPixelValue(position.x), ASCeilPixelValue(position.y));
_position = ASCeilPointValues(position);
} else {
_position = position;
}
@@ -129,7 +146,6 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
[_elementToRectTable setRect:layout.frame forKey:layout.layoutElement];
}
_flattened = NO;
self.retainSublayoutLayoutElements = [ASLayout shouldRetainSublayoutLayoutElements];
}
@@ -173,14 +189,6 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
sublayouts:nil];
}
+ (instancetype)layoutWithLayout:(ASLayout *)layout position:(CGPoint)position
{
return [self layoutWithLayoutElement:layout.layoutElement
size:layout.size
position:position
sublayouts:layout.sublayouts];
}
#pragma mark - Sublayout Elements Caching
- (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements
@@ -207,7 +215,12 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
- (ASLayout *)filteredNodeLayoutTree
{
NSMutableArray *flattenedSublayouts = [NSMutableArray array];
if (ASLayoutIsFlattened(self)) {
// All flattened layouts must have this flag enabled
// to ensure sublayout elements are retained until the layouts are applied.
self.retainSublayoutLayoutElements = YES;
return self;
}
struct Context {
ASLayout *layout;
@@ -216,28 +229,42 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
// Queue used to keep track of sublayouts while traversing this layout in a DFS fashion.
std::deque<Context> queue;
queue.push_front({self, CGPointZero});
while (!queue.empty()) {
Context context = queue.front();
queue.pop_front();
if (self != context.layout && context.layout.type == ASLayoutElementTypeDisplayNode) {
ASLayout *layout = [ASLayout layoutWithLayout:context.layout position:context.absolutePosition];
layout.flattened = YES;
[flattenedSublayouts addObject:layout];
}
std::vector<Context> sublayoutContexts;
for (ASLayout *sublayout in context.layout.sublayouts) {
if (sublayout.isFlattened == NO) {
sublayoutContexts.push_back({sublayout, context.absolutePosition + sublayout.position});
}
}
queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end());
for (ASLayout *sublayout in self.sublayouts) {
queue.push_back({sublayout, sublayout.position});
}
ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size position:CGPointZero sublayouts:flattenedSublayouts];
NSMutableArray *flattenedSublayouts = [NSMutableArray array];
while (!queue.empty()) {
const Context context = queue.front();
queue.pop_front();
ASLayout *layout = context.layout;
const NSArray<ASLayout *> *sublayouts = layout.sublayouts;
const NSUInteger sublayoutsCount = sublayouts.count;
const CGPoint absolutePosition = context.absolutePosition;
if (ASLayoutIsDisplayNodeType(layout)) {
if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) {
// Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position.
layout = [ASLayout layoutWithLayoutElement:layout.layoutElement
size:layout.size
position:absolutePosition
sublayouts:@[]];
}
[flattenedSublayouts addObject:layout];
} else if (sublayoutsCount > 0){
std::vector<Context> sublayoutContexts;
for (ASLayout *sublayout in sublayouts) {
sublayoutContexts.push_back({sublayout, context.absolutePosition + sublayout.position});
}
queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end());
}
}
ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:flattenedSublayouts];
// All flattened layouts must have this flag enabled
// to ensure sublayout elements are retained until the layouts are applied.
layout.retainSublayoutLayoutElements = YES;
return layout;
}
-1
View File
@@ -42,7 +42,6 @@
return [ASLayout layoutWithLayoutElement:layoutElement size:size sublayouts:sublayouts];
} else {
CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode));
// TODO: If it were possible to set .flattened = YES, it would be valid to do so here.
return [ASLayout layoutWithLayoutElement:layoutElement size:size position:position sublayouts:nil];
}
}
+2
View File
@@ -46,6 +46,8 @@ CGSize ASFloorSizeValues(CGSize s);
CGFloat ASFloorPixelValue(CGFloat f);
CGPoint ASCeilPointValues(CGPoint p);
CGSize ASCeilSizeValues(CGSize s);
CGFloat ASCeilPixelValue(CGFloat f);
+5
View File
@@ -153,6 +153,11 @@ CGFloat ASFloorPixelValue(CGFloat f)
return floor(f * scale) / scale;
}
CGPoint ASCeilPointValues(CGPoint p)
{
return CGPointMake(ASCeilPixelValue(p.x), ASCeilPixelValue(p.y));
}
CGSize ASCeilSizeValues(CGSize s)
{
return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height));
+210
View File
@@ -0,0 +1,210 @@
//
// ASLayoutFlatteningTests.m
// Texture
//
// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import <XCTest/XCTest.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>
@interface ASLayoutFlatteningTests : XCTestCase
@end
@implementation ASLayoutFlatteningTests
static ASLayout *layoutWithCustomPosition(CGPoint position, id<ASLayoutElement> element, NSArray<ASLayout *> *sublayouts)
{
return [ASLayout layoutWithLayoutElement:element
size:CGSizeMake(100, 100)
position:position
sublayouts:sublayouts];
}
static ASLayout *layout(id<ASLayoutElement> element, NSArray<ASLayout *> *sublayouts)
{
return layoutWithCustomPosition(CGPointZero, element, sublayouts);
}
- (void)testThatFlattenedLayoutContainsOnlyDirectSubnodesInValidOrder
{
ASLayout *flattenedLayout;
@autoreleasepool {
NSMutableArray<ASDisplayNode *> *subnodes = [NSMutableArray array];
NSMutableArray<ASLayoutSpec *> *layoutSpecs = [NSMutableArray array];
NSMutableArray<ASDisplayNode *> *indirectSubnodes = [NSMutableArray array];
ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; };
ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; };
ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; };
NSArray<ASLayout *> *sublayouts = @[
layout(subnode(), @[
layout(indirectSubnode(), @[]),
]),
layout(layoutSpec(), @[
layout(subnode(), @[]),
layout(layoutSpec(), @[
layout(layoutSpec(), @[]),
layout(subnode(), @[]),
]),
layout(layoutSpec(), @[]),
]),
layout(layoutSpec(), @[
layout(subnode(), @[
layout(indirectSubnode(), @[]),
layout(indirectSubnode(), @[
layout(indirectSubnode(), @[])
]),
])
]),
layout(subnode(), @[]),
];
ASDisplayNode *rootNode = [[ASDisplayNode alloc] init];
ASLayout *originalLayout = [ASLayout layoutWithLayoutElement:rootNode
size:CGSizeMake(1000, 1000)
sublayouts:sublayouts];
flattenedLayout = [originalLayout filteredNodeLayoutTree];
NSArray<ASLayout *> *flattenedSublayouts = flattenedLayout.sublayouts;
NSUInteger sublayoutsCount = flattenedSublayouts.count;
XCTAssertEqualObjects(originalLayout.layoutElement, flattenedLayout.layoutElement, @"The root node should be reserved");
XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null");
XCTAssertEqual(subnodes.count, sublayoutsCount, @"Flattened layout should only contain direct subnodes");
for (int i = 0; i < sublayoutsCount; i++) {
XCTAssertEqualObjects(subnodes[i], flattenedSublayouts[i].layoutElement, @"Sublayouts should be in correct order (flattened in DFS fashion)");
}
}
for (ASLayout *sublayout in flattenedLayout.sublayouts) {
XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained");
XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts");
}
}
#pragma mark - Test reusing ASLayouts while flattening
- (void)testThatLayoutWithNonNullPositionIsNotReused
{
ASDisplayNode *rootNode = [[ASDisplayNode alloc] init];
ASLayout *originalLayout = layoutWithCustomPosition(CGPointMake(10, 10), rootNode, @[]);
ASLayout *flattenedLayout = [originalLayout filteredNodeLayoutTree];
XCTAssertNotEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused");
XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of a root layout should be null");
}
- (void)testThatLayoutWithNullPositionAndNoSublayoutIsReused
{
ASDisplayNode *rootNode = [[ASDisplayNode alloc] init];
ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, rootNode, @[]);
ASLayout *flattenedLayout = [originalLayout filteredNodeLayoutTree];
XCTAssertEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused");
XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of a root layout should be null");
}
- (void)testThatLayoutWithNullPositionAndFlattenedNodeSublayoutsIsReused
{
ASLayout *flattenedLayout;
@autoreleasepool {
ASDisplayNode *rootNode = [[ASDisplayNode alloc] init];
NSMutableArray<ASDisplayNode *> *subnodes = [NSMutableArray array];
ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; };
ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull,
rootNode,
@[
layoutWithCustomPosition(CGPointMake(10, 10), subnode(), @[]),
layoutWithCustomPosition(CGPointMake(20, 20), subnode(), @[]),
layoutWithCustomPosition(CGPointMake(30, 30), subnode(), @[]),
]);
flattenedLayout = [originalLayout filteredNodeLayoutTree];
XCTAssertEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused");
XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null");
}
for (ASLayout *sublayout in flattenedLayout.sublayouts) {
XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained");
XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts");
}
}
- (void)testThatLayoutWithNullPositionAndUnflattenedSublayoutsIsNotReused
{
ASLayout *flattenedLayout;
@autoreleasepool {
ASDisplayNode *rootNode = [[ASDisplayNode alloc] init];
NSMutableArray<ASDisplayNode *> *subnodes = [NSMutableArray array];
NSMutableArray<ASLayoutSpec *> *layoutSpecs = [NSMutableArray array];
NSMutableArray<ASDisplayNode *> *indirectSubnodes = [NSMutableArray array];
NSMutableArray<ASLayout *> *reusedLayouts = [NSMutableArray array];
ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; };
ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; };
ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; };
ASLayout *(^reusedLayout)(ASDisplayNode *) = ^ASLayout *(ASDisplayNode *subnode) { [reusedLayouts addObject:layout(subnode, @[])]; return [reusedLayouts lastObject]; };
/*
* Layouts with sublayouts of both nodes and layout specs should not be reused.
* However, all flattened node sublayouts with valid position should be reused.
*/
ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull,
rootNode,
@[
reusedLayout(subnode()),
// The 2 node sublayouts below should be reused although they are in a layout spec sublayout.
// That is because each of them have an absolute position of zero.
// This case can happen, for example, as the result of a background/overlay layout spec.
layout(layoutSpec(), @[
reusedLayout(subnode()),
reusedLayout(subnode())
]),
layout(subnode(), @[
layout(layoutSpec(), @[])
]),
layout(subnode(), @[
layout(indirectSubnode(), @[])
]),
layoutWithCustomPosition(CGPointMake(10, 10), subnode(), @[]),
// The 2 node sublayouts below shouldn't be reused because they have non-zero absolute positions.
layoutWithCustomPosition(CGPointMake(20, 20), layoutSpec(), @[
layout(subnode(), @[]),
layout(subnode(), @[])
]),
]);
flattenedLayout = [originalLayout filteredNodeLayoutTree];
NSArray<ASLayout *> *flattenedSublayouts = flattenedLayout.sublayouts;
NSUInteger sublayoutsCount = flattenedSublayouts.count;
XCTAssertNotEqualObjects(originalLayout, flattenedLayout, @"Original layout should not be reused");
XCTAssertEqualObjects(originalLayout.layoutElement, flattenedLayout.layoutElement, @"The root node should be reserved");
XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null");
XCTAssertTrue(reusedLayouts.count <= sublayoutsCount, @"Some sublayouts can't be reused");
XCTAssertEqual(subnodes.count, sublayoutsCount, @"Flattened layout should only contain direct subnodes");
int numOfActualReusedLayouts = 0;
for (int i = 0; i < sublayoutsCount; i++) {
ASLayout *sublayout = flattenedSublayouts[i];
XCTAssertEqualObjects(subnodes[i], sublayout.layoutElement, @"Sublayouts should be in correct order (flattened in DFS fashion)");
if ([reusedLayouts containsObject:sublayout]) {
numOfActualReusedLayouts++;
}
}
XCTAssertEqual(numOfActualReusedLayouts, reusedLayouts.count, @"Should reuse all layouts that can be reused");
}
for (ASLayout *sublayout in flattenedLayout.sublayouts) {
XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained");
XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts");
}
}
@end