Merge pull request #1156 from levi/implicitNodeMembership

[ASDisplayNode] Implicit node hierarchy handling with ASLayouts
This commit is contained in:
appleguy
2016-02-01 18:33:32 -08:00
9 changed files with 481 additions and 30 deletions

View File

@@ -475,6 +475,10 @@
D785F6621A74327E00291744 /* ASScrollNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D785F6601A74327E00291744 /* ASScrollNode.h */; settings = {ATTRIBUTES = (Public, ); }; };
D785F6631A74327E00291744 /* ASScrollNode.m in Sources */ = {isa = PBXBuildFile; fileRef = D785F6611A74327E00291744 /* ASScrollNode.m */; };
DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; };
DBC452DB1C5BF64600B16017 /* NSArray+Diffing.h in Headers */ = {isa = PBXBuildFile; fileRef = DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */; };
DBC452DC1C5BF64600B16017 /* NSArray+Diffing.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */; };
DBC452DE1C5C6A6A00B16017 /* ArrayDiffingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */; };
DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */; };
DE040EF91C2B40AC004692FF /* ASCollectionViewFlowLayoutInspector.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF41BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.h */; settings = {ATTRIBUTES = (Public, ); }; };
DE0702FC1C3671E900D7DE62 /* libAsyncDisplayKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AC195D04C000B7D73C /* libAsyncDisplayKit.a */; };
DE6EA3221C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */; };
@@ -802,6 +806,10 @@
D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = "<group>"; };
D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = "<group>"; };
D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = "<group>"; };
DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Diffing.h"; sourceTree = "<group>"; };
DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Diffing.m"; sourceTree = "<group>"; };
DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArrayDiffingTests.m; sourceTree = "<group>"; };
DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeImplicitHierarchyTests.m; sourceTree = "<group>"; };
DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkPrivate.h"; sourceTree = "<group>"; };
DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDelegateProxy.h; sourceTree = "<group>"; };
DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDelegateProxy.m; sourceTree = "<group>"; };
@@ -1000,6 +1008,8 @@
058D09C5195D04C000B7D73C /* AsyncDisplayKitTests */ = {
isa = PBXGroup;
children = (
DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */,
DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */,
057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */,
056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */,
05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */,
@@ -1164,6 +1174,8 @@
0442850C1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.mm */,
AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */,
AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */,
DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */,
DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */,
);
path = Private;
sourceTree = "<group>";
@@ -1354,6 +1366,7 @@
ACF6ED201B17843500DA7C62 /* ASDimension.h in Headers */,
058D0A78195D05F900B7D73C /* ASDisplayNode+DebugTiming.h in Headers */,
DECBD6E71BE56E1900CF4905 /* ASButtonNode.h in Headers */,
DBC452DB1C5BF64600B16017 /* NSArray+Diffing.h in Headers */,
058D0A4C195D05CB00B7D73C /* ASDisplayNode+Subclasses.h in Headers */,
258FF4271C0D152600A83844 /* ASRangeHandlerVisible.h in Headers */,
058D0A4A195D05CB00B7D73C /* ASDisplayNode.h in Headers */,
@@ -1798,6 +1811,7 @@
ACF6ED1D1B17843500DA7C62 /* ASCenterLayoutSpec.mm in Sources */,
18C2ED801B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */,
92DD2FE41BF4B97E0074C9DD /* ASMapNode.mm in Sources */,
DBC452DC1C5BF64600B16017 /* NSArray+Diffing.m in Sources */,
AC3C4A521A1139C100143C57 /* ASCollectionView.mm in Sources */,
205F0E1E1B373A2C007741D0 /* ASCollectionViewLayoutController.mm in Sources */,
058D0A13195D050800B7D73C /* ASControlNode.m in Sources */,
@@ -1906,7 +1920,9 @@
254C6B521BF8FE6D003EC431 /* ASTextKitTruncationTests.mm in Sources */,
058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */,
058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */,
DBC453221C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m in Sources */,
058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */,
DBC452DE1C5C6A6A00B16017 /* ArrayDiffingTests.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -11,6 +11,9 @@
+ (BOOL)shouldUseNewRenderingRange;
+ (void)setShouldUseNewRenderingRange:(BOOL)shouldUseNewRenderingRange;
+ (BOOL)usesImplicitHierarchyManagement;
+ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled;
/** @name Layout */

View File

@@ -21,6 +21,7 @@
#import "_ASCoreAnimationExtras.h"
#import "ASDisplayNodeExtras.h"
#import "ASEqualityHelpers.h"
#import "NSArray+Diffing.h"
#import "ASInternalHelpers.h"
#import "ASLayout.h"
@@ -29,6 +30,36 @@
NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
@interface _ASDisplayNodePosition : NSObject
@property (nonatomic, assign) NSUInteger index;
@property (nonatomic, strong) ASDisplayNode *node;
+ (instancetype)positionWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index;
- (instancetype)initWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index;
@end
@implementation _ASDisplayNodePosition
+ (instancetype)positionWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index
{
return [[self alloc] initWithNode:node atIndex:index];
}
- (instancetype)initWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index
{
self = [super init];
if (self) {
_node = node;
_index = index;
}
return self;
}
@end
@interface ASDisplayNode () <UIGestureRecognizerDelegate>
/**
@@ -52,6 +83,9 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
#endif
@interface ASDisplayNode () <_ASDisplayLayerDelegate>
@property (assign, nonatomic) BOOL implicitNodeHierarchyManagement;
@end
@implementation ASDisplayNode
@@ -62,9 +96,21 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
@synthesize preferredFrameSize = _preferredFrameSize;
@synthesize isFinalLayoutable = _isFinalLayoutable;
static BOOL usesImplicitHierarchyManagement = FALSE;
+ (BOOL)usesImplicitHierarchyManagement
{
return usesImplicitHierarchyManagement;
}
+ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled
{
usesImplicitHierarchyManagement = enabled;
}
BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector)
{
return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector);
return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector);
}
void ASDisplayNodeRespectThreadAffinityOfNode(ASDisplayNode *node, void (^block)())
@@ -582,14 +628,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
}
- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
{
ASDN::MutexLocker l(_propertyLock);
return [self __measureWithSizeRange:constrainedSize];
}
- (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize
{
ASDisplayNodeAssertThreadAffinity(self);
ASDN::MutexLocker l(_propertyLock);
if (![self __shouldSize])
return nil;
@@ -598,7 +639,22 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
// - we haven't already
// - the constrained size range is different
if (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)) {
_layout = [self calculateLayoutThatFits:constrainedSize];
ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize];
if (_layout) {
NSIndexSet *insertions, *deletions;
[_layout.sublayouts asdk_diffWithArray:newLayout.sublayouts insertions:&insertions deletions:&deletions compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) {
return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject);
}];
_insertedSubnodes = [self _filterNodesInLayouts:newLayout.sublayouts withIndexes:insertions];
_deletedSubnodes = [self _filterNodesInLayouts:_layout.sublayouts withIndexes:deletions];
} else {
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [newLayout.sublayouts count])];
_insertedSubnodes = [self _filterNodesInLayouts:newLayout.sublayouts withIndexes:indexes];
_deletedSubnodes = @[];
}
_layout = newLayout;
_constrainedSize = constrainedSize;
_flags.isMeasured = YES;
[self calculatedLayoutDidChange];
@@ -608,23 +664,36 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
ASDisplayNodeAssertTrue(_layout.size.width >= 0.0);
ASDisplayNodeAssertTrue(_layout.size.height >= 0.0);
// we generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go
// also if a node has no size, it should not have a placeholder
if (self.placeholderEnabled && [self _displaysAsynchronously] && _layout.size.width > 0.0 && _layout.size.height > 0.0) {
// we generate placeholders at measureWithSizeRange: time so that a node is guaranteed
// to have a placeholder ready to go. Also, if a node has no size it should not have a placeholder
if (self.placeholderEnabled && [self _displaysAsynchronously] &&
_layout.size.width > 0.0 && _layout.size.height > 0.0) {
if (!_placeholderImage) {
_placeholderImage = [self placeholderImage];
}
if (_placeholderLayer) {
[self setupPlaceholderLayerContents];
[self _setupPlaceholderLayerContents];
}
}
return _layout;
}
- (NSArray<_ASDisplayNodePosition *> *)_filterNodesInLayouts:(NSArray<ASLayout *> *)layouts withIndexes:(NSIndexSet *)indexes
{
NSMutableArray<_ASDisplayNodePosition *> *result = [NSMutableArray array];
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
ASDisplayNode *node = (ASDisplayNode *)layouts[idx].layoutableObject;
ASDisplayNodeAssertNotNil(node, @"A flattened layout must consist exclusively of node sublayouts");
[result addObject:[_ASDisplayNodePosition positionWithNode:node atIndex:idx]];
}];
return result;
}
- (void)calculatedLayoutDidChange
{
// subclass override
}
- (BOOL)displaysAsynchronously
@@ -805,7 +874,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
ASDisplayNodeAssertMainThread();
ASDN::MutexLocker l(_propertyLock);
if (CGRectEqualToRect(self.bounds, CGRectZero)) {
return; // Performing layout on a zero-bounds view often results in frame calculations with negative sizes after applying margins, which will cause measureWithSizeRange: on subnodes to assert.
// Performing layout on a zero-bounds view often results in frame calculations
// with negative sizes after applying margins, which will cause
// measureWithSizeRange: on subnodes to assert.
return;
}
_placeholderLayer.frame = self.bounds;
[self layout];
@@ -1606,7 +1678,11 @@ static BOOL ShouldUseNewRenderingRange = YES;
layout = [ASLayout layoutWithLayoutableObject:self size:layout.size sublayouts:@[layout]];
}
return [layout flattenedLayoutUsingPredicateBlock:^BOOL(ASLayout *evaluatedLayout) {
return [_subnodes containsObject:evaluatedLayout.layoutableObject];
if ([[self class] usesImplicitHierarchyManagement]) {
return ASObjectIsEqual(layout, evaluatedLayout) == NO && [evaluatedLayout.layoutableObject isKindOfClass:[ASDisplayNode class]];
} else {
return [_subnodes containsObject:evaluatedLayout.layoutableObject];
}
}];
} else {
// If neither -layoutSpecThatFits: nor -calculateSizeThatFits: is overridden by subclassses, preferredFrameSize should be used,
@@ -1974,7 +2050,9 @@ static BOOL ShouldUseNewRenderingRange = YES;
ASDisplayNode *subnode = nil;
CGRect subnodeFrame = CGRectZero;
for (ASLayout *subnodeLayout in _layout.sublayouts) {
ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Cached sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes);
if (![[self class] usesImplicitHierarchyManagement]) {
ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes);
}
CGPoint adjustedOrigin = subnodeLayout.position;
if (isfinite(adjustedOrigin.x) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
@@ -2000,6 +2078,45 @@ static BOOL ShouldUseNewRenderingRange = YES;
subnode = ((ASDisplayNode *)subnodeLayout.layoutableObject);
[subnode setFrame:subnodeFrame];
}
if ([[self class] usesImplicitHierarchyManagement]) {
for (_ASDisplayNodePosition *position in _deletedSubnodes) {
[self _implicitlyRemoveSubnode:position.node atIndex:position.index];
}
for (_ASDisplayNodePosition *position in _insertedSubnodes) {
[self _implicitlyInsertSubnode:position.node atIndex:position.index];
}
}
}
- (void)_implicitlyInsertSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx
{
ASDisplayNodeAssertThreadAffinity(self);
if (!_managedSubnodes) {
_managedSubnodes = [NSMutableArray array];
}
ASDisplayNodeAssert(idx <= [_managedSubnodes count], @"index needs to be in range of the current managed subnodes");
if (idx == [_managedSubnodes count]) {
[_managedSubnodes addObject:node];
} else {
[_managedSubnodes insertObject:node atIndex:idx];
}
[self addSubnode:node];
}
- (void)_implicitlyRemoveSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx
{
ASDisplayNodeAssertThreadAffinity(self);
if (!_managedSubnodes) {
_managedSubnodes = [NSMutableArray array];
}
[_managedSubnodes removeObjectAtIndex:idx];
[node removeFromSupernode];
}
- (void)displayWillStart
@@ -2012,14 +2129,14 @@ static BOOL ShouldUseNewRenderingRange = YES;
if (_placeholderImage && _placeholderLayer && self.layer.contents == nil) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self setupPlaceholderLayerContents];
[self _setupPlaceholderLayerContents];
_placeholderLayer.opacity = 1.0;
[CATransaction commit];
[self.layer addSublayer:_placeholderLayer];
}
}
- (void)setupPlaceholderLayerContents
- (void)_setupPlaceholderLayerContents
{
BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(_placeholderImage.capInsets, UIEdgeInsetsZero);
if (stretchable) {

View File

@@ -12,7 +12,7 @@
#import "ASAssert.h"
#import "ASLayoutSpecUtilities.h"
#import "ASInternalHelpers.h"
#import <stack>
#import <queue>
CGPoint const CGPointNull = {NAN, NAN};
@@ -71,14 +71,14 @@ extern BOOL CGPointIsNull(CGPoint point)
BOOL visited;
};
// Stack of Contexts, used to keep track of sublayouts while traversing this layout in a DFS fashion.
std::stack<Context> stack;
stack.push({self, CGPointMake(0, 0), NO});
// Stack of Contexts, used to keep track of sublayouts while traversing this layout in a BFS fashion.
std::queue<Context> queue;
queue.push({self, CGPointMake(0, 0), NO});
while (!stack.empty()) {
Context &context = stack.top();
while (!queue.empty()) {
Context &context = queue.front();
if (context.visited) {
stack.pop();
queue.pop();
} else {
context.visited = YES;
@@ -90,11 +90,11 @@ extern BOOL CGPointIsNull(CGPoint point)
}
for (ASLayout *sublayout in context.layout.sublayouts) {
stack.push({sublayout, context.absolutePosition + sublayout.position, NO});
queue.push({sublayout, context.absolutePosition + sublayout.position, NO});
}
}
}
return [ASLayout layoutWithLayoutableObject:_layoutableObject size:_size sublayouts:flattenedSublayouts];
}

View File

@@ -35,6 +35,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
};
@class _ASPendingState;
@class _ASDisplayNodePosition;
// Allow 2^n increments of begin disabling hierarchy notifications
#define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4
@@ -60,6 +61,12 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
UIEdgeInsets _hitTestSlop;
NSMutableArray *_subnodes;
// Subnodes implicitly managed by layout changes
NSMutableArray<ASDisplayNode *> *_managedSubnodes;
NSArray<_ASDisplayNodePosition *> *_insertedSubnodes;
NSArray<_ASDisplayNodePosition *> *_deletedSubnodes;
ASDisplayNodeViewBlock _viewBlock;
ASDisplayNodeLayerBlock _layerBlock;
ASDisplayNodeDidLoadBlock _nodeLoadedBlock;
@@ -131,10 +138,11 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
- (BOOL)__shouldLoadViewOrLayer;
- (BOOL)__shouldSize;
// Core implementation of -measureWithSizeRange:. Must be called with _propertyLock held.
- (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize;
/**
Invoked by a call to setNeedsLayout to the underlying view
*/
- (void)__setNeedsLayout;
- (void)__layout;
- (void)__setSupernode:(ASDisplayNode *)supernode;

View File

@@ -0,0 +1,29 @@
//
// NSArray+Diffing.h
// AsyncDisplayKit
//
// Created by Levi McCallum on 1/29/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSArray (Diffing)
/**
* @abstract Compares two arrays, providing the insertion and deletion indexes needed to transform into the target array.
* @discussion This compares the equality of each object with `isEqual:`.
* This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences.
* It runs in O(mn) complexity.
*/
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions;
/**
* @abstract Compares two arrays, providing the insertion and deletion indexes needed to transform into the target array.
* @discussion The `compareBlock` is used to identify the equality of the objects within the arrays.
* This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences.
* It runs in O(mn) complexity.
*/
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison;
@end

View File

@@ -0,0 +1,78 @@
//
// NSArray+Diffing.m
// AsyncDisplayKit
//
// Created by Levi McCallum on 1/29/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import "NSArray+Diffing.h"
@implementation NSArray (Diffing)
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
{
[self asdk_diffWithArray:array insertions:insertions deletions:deletions compareBlock:^BOOL(id lhs, id rhs) {
return [lhs isEqual:rhs];
}];
}
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison
{
NSIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison];
if (insertions) {
NSArray *commonObjects = [self objectsAtIndexes:commonIndexes];
NSMutableIndexSet *insertionIndexes = [NSMutableIndexSet indexSet];
for (NSInteger i = 0, j = 0; i < commonObjects.count || j < array.count;) {
if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) {
i++; j++;
} else {
[insertionIndexes addIndex:j];
j++;
}
}
*insertions = insertionIndexes;
}
if (deletions) {
NSMutableIndexSet *deletionIndexes = [NSMutableIndexSet indexSet];
for (NSInteger i = 0; i < self.count; i++) {
if (![commonIndexes containsIndex:i]) {
[deletionIndexes addIndex:i];
}
}
*deletions = deletionIndexes;
}
}
- (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison
{
NSInteger lengths[self.count+1][array.count+1];
for (NSInteger i = self.count; i >= 0; i--) {
for (NSInteger j = array.count; j >= 0; j--) {
if (i == self.count || j == array.count) {
lengths[i][j] = 0;
} else if ([self[i] isEqual:array[j]]) {
lengths[i][j] = 1 + lengths[i+1][j+1];
} else {
lengths[i][j] = MAX(lengths[i+1][j], lengths[i][j+1]);
}
}
}
NSMutableIndexSet *common = [NSMutableIndexSet indexSet];
for (NSInteger i = 0, j = 0; i < self.count && j < array.count;) {
if (comparison(self[i], array[j])) {
[common addIndex:i];
i++; j++;
} else if (lengths[i+1][j] >= lengths[i][j+1]) {
i++;
} else {
j++;
}
}
return common;
}
@end

View File

@@ -0,0 +1,129 @@
//
// ASDisplayNodeImplicitHierarchyTests.m
// AsyncDisplayKit
//
// Created by Levi McCallum on 2/1/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <XCTest/XCTest.h>
#import "ASDisplayNode.h"
#import "ASDisplayNode+Beta.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASStaticLayoutSpec.h"
#import "ASStackLayoutSpec.h"
@interface ASSpecTestDisplayNode : ASDisplayNode
@property (copy, nonatomic) ASLayoutSpec * (^layoutSpecBlock)(ASSizeRange constrainedSize, NSNumber *layoutState);
/**
Simple state identifier to allow control of current spec inside of the layoutSpecBlock
*/
@property (strong, nonatomic) NSNumber *layoutState;
@end
@implementation ASSpecTestDisplayNode
- (instancetype)init
{
self = [super init];
if (self) {
_layoutState = @1;
}
return self;
}
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
return self.layoutSpecBlock(constrainedSize, _layoutState);
}
@end
@interface ASDisplayNodeImplicitHierarchyTests : XCTestCase
@end
@implementation ASDisplayNodeImplicitHierarchyTests
- (void)setUp {
[super setUp];
[ASDisplayNode setUsesImplicitHierarchyManagement:YES];
}
- (void)tearDown {
[ASDisplayNode setUsesImplicitHierarchyManagement:NO];
[super tearDown];
}
- (void)testFeatureFlag
{
XCTAssert([ASDisplayNode usesImplicitHierarchyManagement]);
}
- (void)testInitialNodeInsertionWithOrdering
{
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
ASDisplayNode *node3 = [[ASDisplayNode alloc] init];
ASDisplayNode *node4 = [[ASDisplayNode alloc] init];
ASDisplayNode *node5 = [[ASDisplayNode alloc] init];
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
node.layoutSpecBlock = ^(ASSizeRange constrainedSize, NSNumber *layoutState) {
ASStaticLayoutSpec *staticLayout = [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node4]];
ASStackLayoutSpec *stack1 = [[ASStackLayoutSpec alloc] init];
[stack1 setChildren:@[node1, node2]];
ASStackLayoutSpec *stack2 = [[ASStackLayoutSpec alloc] init];
[stack2 setChildren:@[node3, staticLayout]];
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[stack1, stack2, node5]];
};
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
[node layout]; // Layout immediately
XCTAssertEqual(node.subnodes[0], node5);
XCTAssertEqual(node.subnodes[1], node1);
XCTAssertEqual(node.subnodes[2], node2);
XCTAssertEqual(node.subnodes[3], node3);
XCTAssertEqual(node.subnodes[4], node4);
}
- (void)testCalculatedLayoutHierarchyTransitions
{
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
ASDisplayNode *node3 = [[ASDisplayNode alloc] init];
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
node.layoutSpecBlock = ^(ASSizeRange constrainedSize, NSNumber *layoutState){
if ([layoutState isEqualToNumber:@1]) {
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1, node2]];
} else {
ASStackLayoutSpec *stackLayout = [[ASStackLayoutSpec alloc] init];
[stackLayout setChildren:@[node3, node2]];
return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1, stackLayout]];
}
};
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
[node layout]; // Layout immediately
XCTAssertEqual(node.subnodes[0], node1);
XCTAssertEqual(node.subnodes[1], node2);
node.layoutState = @2;
[node invalidateCalculatedLayout]; // TODO(levi): Look into a way where measureWithSizeRange resizes when a new hierarchy is introduced but the size has not changed
[node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)];
[node layout]; // Layout immediately
XCTAssertEqual(node.subnodes[0], node1);
XCTAssertEqual(node.subnodes[1], node3);
XCTAssertEqual(node.subnodes[2], node2);
}
@end

View File

@@ -0,0 +1,71 @@
//
// ArrayDiffingTests.m
// AsyncDisplayKit
//
// Created by Levi McCallum on 1/29/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <XCTest/XCTest.h>
#import "NSArray+Diffing.h"
@interface ArrayDiffingTests : XCTestCase
@end
@implementation ArrayDiffingTests
- (void)testDiffing {
NSArray<NSArray *> *tests = @[
@[
@[@"bob", @"alice", @"dave"],
@[@"bob", @"alice", @"dave", @"gary"],
@[@3],
@[],
],
@[
@[@"bob", @"alice", @"dave"],
@[@"bob", @"gary", @"alice", @"dave"],
@[@1],
@[],
],
@[
@[@"bob", @"alice", @"dave"],
@[@"bob", @"alice"],
@[],
@[@2],
],
@[
@[@"bob", @"alice", @"dave"],
@[],
@[],
@[@0, @1, @2],
],
@[
@[@"bob", @"alice", @"dave"],
@[@"gary", @"alice", @"dave", @"jack"],
@[@0, @3],
@[@0],
],
@[
@[@"bob", @"alice", @"dave", @"judy", @"lynda", @"tony"],
@[@"gary", @"bob", @"suzy", @"tony"],
@[@0, @2],
@[@1, @2, @3, @4],
],
];
for (NSArray *test in tests) {
NSIndexSet *insertions, *deletions;
[test[0] asdk_diffWithArray:test[1] insertions:&insertions deletions:&deletions];
for (NSNumber *index in (NSArray *)test[2]) {
XCTAssert([insertions containsIndex:[index integerValue]]);
}
for (NSNumber *index in (NSArray *)test[3]) {
XCTAssert([deletions containsIndex:[index integerValue]]);
}
}
}
@end