diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 2913d173c8..f577bfe88c 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -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 = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASScrollNode.m; sourceTree = ""; }; + DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Diffing.h"; sourceTree = ""; }; + DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Diffing.m"; sourceTree = ""; }; + DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ArrayDiffingTests.m; sourceTree = ""; }; + DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeImplicitHierarchyTests.m; sourceTree = ""; }; DE6EA3211C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+FrameworkPrivate.h"; sourceTree = ""; }; DE8BEABF1C2DF3FC00D57C12 /* ASDelegateProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDelegateProxy.h; sourceTree = ""; }; DE8BEAC01C2DF3FC00D57C12 /* ASDelegateProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDelegateProxy.m; sourceTree = ""; }; @@ -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 = ""; @@ -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; }; diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index 378c060455..d31ee49504 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -11,6 +11,9 @@ + (BOOL)shouldUseNewRenderingRange; + (void)setShouldUseNewRenderingRange:(BOOL)shouldUseNewRenderingRange; ++ (BOOL)usesImplicitHierarchyManagement; ++ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled; + /** @name Layout */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index cd0cb2fb96..18753cf85c 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -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 () /** @@ -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 *)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) { diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index 745b325e52..e02e618c7a 100644 --- a/AsyncDisplayKit/Layout/ASLayout.mm +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -12,7 +12,7 @@ #import "ASAssert.h" #import "ASLayoutSpecUtilities.h" #import "ASInternalHelpers.h" -#import +#import 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 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 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]; } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index d688289e15..e0740fd3fc 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -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 *_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; diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.h b/AsyncDisplayKit/Private/NSArray+Diffing.h new file mode 100644 index 0000000000..a549d45f49 --- /dev/null +++ b/AsyncDisplayKit/Private/NSArray+Diffing.h @@ -0,0 +1,29 @@ +// +// NSArray+Diffing.h +// AsyncDisplayKit +// +// Created by Levi McCallum on 1/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@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 diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.m b/AsyncDisplayKit/Private/NSArray+Diffing.m new file mode 100644 index 0000000000..00893d1416 --- /dev/null +++ b/AsyncDisplayKit/Private/NSArray+Diffing.m @@ -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 diff --git a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m new file mode 100644 index 0000000000..c565297d7c --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m @@ -0,0 +1,129 @@ +// +// ASDisplayNodeImplicitHierarchyTests.m +// AsyncDisplayKit +// +// Created by Levi McCallum on 2/1/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +#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 diff --git a/AsyncDisplayKitTests/ArrayDiffingTests.m b/AsyncDisplayKitTests/ArrayDiffingTests.m new file mode 100644 index 0000000000..d2224e0baa --- /dev/null +++ b/AsyncDisplayKitTests/ArrayDiffingTests.m @@ -0,0 +1,71 @@ +// +// ArrayDiffingTests.m +// AsyncDisplayKit +// +// Created by Levi McCallum on 1/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +#import "NSArray+Diffing.h" + +@interface ArrayDiffingTests : XCTestCase + +@end + +@implementation ArrayDiffingTests + +- (void)testDiffing { + 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