From 184d1fc05997558e5acf28e665240dfc594ef521 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Thu, 28 Jan 2016 23:38:18 -0800 Subject: [PATCH 01/13] Switch layout flatten to BFS for node ordering --- AsyncDisplayKit/ASDisplayNode.mm | 2 +- AsyncDisplayKit/Layout/ASLayout.mm | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index cd0cb2fb96..5caf4f49b9 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -64,7 +64,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) { - return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); + return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); } void ASDisplayNodeRespectThreadAffinityOfNode(ASDisplayNode *node, void (^block)()) diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index 745b325e52..4a48501a97 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,7 +90,7 @@ 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}); } } } From b2843d29c4859fea3ce3e0b807e2f01e23d51b6c Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Thu, 28 Jan 2016 23:54:05 -0800 Subject: [PATCH 02/13] Allow any node to be identified in the flattened predicate search --- AsyncDisplayKit/ASDisplayNode.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 5caf4f49b9..1983befffa 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1606,7 +1606,8 @@ static BOOL ShouldUseNewRenderingRange = YES; layout = [ASLayout layoutWithLayoutableObject:self size:layout.size sublayouts:@[layout]]; } return [layout flattenedLayoutUsingPredicateBlock:^BOOL(ASLayout *evaluatedLayout) { - return [_subnodes containsObject:evaluatedLayout.layoutableObject]; + return ASObjectIsEqual(layout, evaluatedLayout) == NO && + [evaluatedLayout.layoutableObject isKindOfClass:[ASDisplayNode class]]; }]; } else { // If neither -layoutSpecThatFits: nor -calculateSizeThatFits: is overridden by subclassses, preferredFrameSize should be used, From 561ae212d9a14e5c56a6deb2cf8633627ab87b11 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Fri, 29 Jan 2016 10:50:49 -0800 Subject: [PATCH 03/13] Wrap implicit hierarchy management in a class enable bit --- AsyncDisplayKit/ASDisplayNode+Beta.h | 3 +++ AsyncDisplayKit/ASDisplayNode.mm | 21 +++++++++++++++++++-- AsyncDisplayKit/Layout/ASLayout.mm | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) 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 1983befffa..b554995181 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -52,6 +52,9 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; #endif @interface ASDisplayNode () <_ASDisplayLayerDelegate> + +@property (assign, nonatomic) BOOL implicitNodeHierarchyManagement; + @end @implementation ASDisplayNode @@ -62,6 +65,17 @@ 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); @@ -1606,8 +1620,11 @@ static BOOL ShouldUseNewRenderingRange = YES; layout = [ASLayout layoutWithLayoutableObject:self size:layout.size sublayouts:@[layout]]; } return [layout flattenedLayoutUsingPredicateBlock:^BOOL(ASLayout *evaluatedLayout) { - return ASObjectIsEqual(layout, evaluatedLayout) == NO && - [evaluatedLayout.layoutableObject isKindOfClass:[ASDisplayNode class]]; + 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, diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index 4a48501a97..e02e618c7a 100644 --- a/AsyncDisplayKit/Layout/ASLayout.mm +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -94,7 +94,7 @@ extern BOOL CGPointIsNull(CGPoint point) } } } - + return [ASLayout layoutWithLayoutableObject:_layoutableObject size:_size sublayouts:flattenedSublayouts]; } From 29609bfe87d0953c203b508bfe4ed945d0815356 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Fri, 29 Jan 2016 10:51:46 -0800 Subject: [PATCH 04/13] Clean up long lines --- AsyncDisplayKit/ASDisplayNode.mm | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index b554995181..a3a572f570 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -622,16 +622,11 @@ 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) { - if (!_placeholderImage) { - _placeholderImage = [self placeholderImage]; - } - - if (_placeholderLayer) { - [self setupPlaceholderLayerContents]; - } + // 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) { + [self __generatePlaceholder]; } return _layout; @@ -639,6 +634,18 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (void)calculatedLayoutDidChange { + // subclass override +} + +- (void)__generatePlaceholder +{ + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + + if (_placeholderLayer) { + [self setupPlaceholderLayerContents]; + } } - (BOOL)displaysAsynchronously @@ -819,7 +826,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]; From 822fc96f96cfd94f5bd227e48fbb8e41d969fc8a Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Fri, 29 Jan 2016 12:04:54 -0800 Subject: [PATCH 05/13] Add LCS diffing support to NSArray --- AsyncDisplayKit.xcodeproj/project.pbxproj | 8 +++ AsyncDisplayKit/Private/NSArray+Diffing.h | 18 ++++++ AsyncDisplayKit/Private/NSArray+Diffing.m | 67 +++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 AsyncDisplayKit/Private/NSArray+Diffing.h create mode 100644 AsyncDisplayKit/Private/NSArray+Diffing.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 0091e38901..16393ae17f 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -475,6 +475,8 @@ 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 */; }; 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 +804,8 @@ 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 = ""; }; 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 = ""; }; @@ -1164,6 +1168,8 @@ 0442850C1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.mm */, AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */, AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */, + DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */, + DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */, ); path = Private; sourceTree = ""; @@ -1354,6 +1360,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 */, @@ -1782,6 +1789,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 */, diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.h b/AsyncDisplayKit/Private/NSArray+Diffing.h new file mode 100644 index 0000000000..618e1901ea --- /dev/null +++ b/AsyncDisplayKit/Private/NSArray+Diffing.h @@ -0,0 +1,18 @@ +// +// NSArray+Diffing.h +// AsyncDisplayKit +// +// Created by Levi McCallum on 1/29/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface NSArray (Diffing) + +/** + * Uses a bottom-up memoized longest common subsequence solution to identify differences. Runs in O(mn) complexity. + */ +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSMutableIndexSet **)insertions deletions:(NSMutableIndexSet **)deletions; + +@end diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.m b/AsyncDisplayKit/Private/NSArray+Diffing.m new file mode 100644 index 0000000000..f320bcfb58 --- /dev/null +++ b/AsyncDisplayKit/Private/NSArray+Diffing.m @@ -0,0 +1,67 @@ +// +// 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:(NSMutableIndexSet **)insertions deletions:(NSMutableIndexSet **)deletions +{ + NSIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array]; + + if (insertions) { + NSArray *commonObjects = [self objectsAtIndexes:commonIndexes]; + for (NSInteger i = 0, j = 0; i < commonObjects.count || j < array.count;) { + if (i < commonObjects.count && j < array.count && [commonObjects[i] isEqual:array[j]]) { + i++; j++; + } else { + [*insertions addIndex:j]; + j++; + } + } + } + + if (deletions) { + for (NSInteger i = 0; i < self.count; i++) { + if (![commonIndexes containsIndex:i]) { + [*deletions addIndex:i]; + } + } + } +} + +- (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array +{ + 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 ([self[i] isEqual:array[j]]) { + [common addIndex:i]; + i++; j++; + } else if (lengths[i+1][j] >= lengths[i][j+1]) { + i++; + } else { + j++; + } + } + return common; +} + +@end From 7a3987a467a7052df1fc7e567f6591be81ad561c Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Fri, 29 Jan 2016 20:15:08 -0800 Subject: [PATCH 06/13] Add tests to LCS array category --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++ AsyncDisplayKitTests/ArrayDiffingTests.m | 72 +++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 AsyncDisplayKitTests/ArrayDiffingTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 16393ae17f..fca5e26e5f 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -477,6 +477,7 @@ 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 */; }; 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 */; }; @@ -806,6 +807,7 @@ 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 = ""; }; 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 = ""; }; @@ -1004,6 +1006,7 @@ 058D09C5195D04C000B7D73C /* AsyncDisplayKitTests */ = { isa = PBXGroup; children = ( + DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, 056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */, 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */, @@ -1899,6 +1902,7 @@ 058D0A3D195D057000B7D73C /* ASTextKitCoreTextAdditionsTests.m in Sources */, 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */, 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */, + DBC452DE1C5C6A6A00B16017 /* ArrayDiffingTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AsyncDisplayKitTests/ArrayDiffingTests.m b/AsyncDisplayKitTests/ArrayDiffingTests.m new file mode 100644 index 0000000000..c83e841051 --- /dev/null +++ b/AsyncDisplayKitTests/ArrayDiffingTests.m @@ -0,0 +1,72 @@ +// +// 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) { + NSMutableIndexSet *insertions = [NSMutableIndexSet indexSet]; + NSMutableIndexSet *deletions = [NSMutableIndexSet indexSet]; + [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 From 3abe6d9181c2d3ea8ed4c7da3d4fbdb05a1be6fc Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Sun, 31 Jan 2016 20:59:59 -0800 Subject: [PATCH 07/13] Simplify measure call structure --- AsyncDisplayKit/ASDisplayNode.mm | 7 +------ AsyncDisplayKit/Private/ASDisplayNodeInternal.h | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index a3a572f570..4d57bf1a48 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -596,14 +596,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; diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index d688289e15..4b72f2c097 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -131,9 +131,7 @@ 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; From 924e72f7740099493f5c8a88f95713fc23f3e4cb Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Sun, 31 Jan 2016 21:01:41 -0800 Subject: [PATCH 08/13] Mark setup placeholder method as private --- AsyncDisplayKit/ASDisplayNode.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 4d57bf1a48..032e2fecd4 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -639,7 +639,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } if (_placeholderLayer) { - [self setupPlaceholderLayerContents]; + [self _setupPlaceholderLayerContents]; } } @@ -2035,14 +2035,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) { From e852cb612c0b1d5b26818fae0b3f2e7f83b7ff85 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Sun, 31 Jan 2016 21:17:24 -0800 Subject: [PATCH 09/13] Simplify usage of diffing API --- AsyncDisplayKit/ASDisplayNode.mm | 1 + AsyncDisplayKit/Private/NSArray+Diffing.h | 2 +- AsyncDisplayKit/Private/NSArray+Diffing.m | 10 +++++++--- AsyncDisplayKitTests/ArrayDiffingTests.m | 3 +-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 032e2fecd4..bfd4cd3584 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" diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.h b/AsyncDisplayKit/Private/NSArray+Diffing.h index 618e1901ea..374096f92f 100644 --- a/AsyncDisplayKit/Private/NSArray+Diffing.h +++ b/AsyncDisplayKit/Private/NSArray+Diffing.h @@ -13,6 +13,6 @@ /** * Uses a bottom-up memoized longest common subsequence solution to identify differences. Runs in O(mn) complexity. */ -- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSMutableIndexSet **)insertions deletions:(NSMutableIndexSet **)deletions; +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions; @end diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.m b/AsyncDisplayKit/Private/NSArray+Diffing.m index f320bcfb58..0cd9ad040b 100644 --- a/AsyncDisplayKit/Private/NSArray+Diffing.m +++ b/AsyncDisplayKit/Private/NSArray+Diffing.m @@ -10,28 +10,32 @@ @implementation NSArray (Diffing) -- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSMutableIndexSet **)insertions deletions:(NSMutableIndexSet **)deletions +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions { NSIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array]; 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 && [commonObjects[i] isEqual:array[j]]) { i++; j++; } else { - [*insertions addIndex:j]; + [insertionIndexes addIndex:j]; j++; } } + *insertions = insertionIndexes; } if (deletions) { + NSMutableIndexSet *deletionIndexes = [NSMutableIndexSet indexSet]; for (NSInteger i = 0; i < self.count; i++) { if (![commonIndexes containsIndex:i]) { - [*deletions addIndex:i]; + [deletionIndexes addIndex:i]; } } + *deletions = deletionIndexes; } } diff --git a/AsyncDisplayKitTests/ArrayDiffingTests.m b/AsyncDisplayKitTests/ArrayDiffingTests.m index c83e841051..d2224e0baa 100644 --- a/AsyncDisplayKitTests/ArrayDiffingTests.m +++ b/AsyncDisplayKitTests/ArrayDiffingTests.m @@ -57,8 +57,7 @@ ]; for (NSArray *test in tests) { - NSMutableIndexSet *insertions = [NSMutableIndexSet indexSet]; - NSMutableIndexSet *deletions = [NSMutableIndexSet indexSet]; + 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]]); From 9f25b54f9eb6d6e7b36da987706304526aa577e3 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Mon, 1 Feb 2016 10:51:01 -0800 Subject: [PATCH 10/13] Support insertion on first layout of display node --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++ AsyncDisplayKit/ASDisplayNode.mm | 34 +++++++++- .../Private/ASDisplayNodeInternal.h | 7 +- .../ASDisplayNodeImplicitHierarchyTests.m | 66 +++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index fca5e26e5f..5f44fe5f5e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -478,6 +478,7 @@ 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 */; }; @@ -808,6 +809,7 @@ 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 = ""; }; @@ -1006,6 +1008,7 @@ 058D09C5195D04C000B7D73C /* AsyncDisplayKitTests */ = { isa = PBXGroup; children = ( + DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */, DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */, 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, 056D21501ABCEDA1001107EF /* ASSnapshotTestCase.h */, @@ -1901,6 +1904,7 @@ 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 */, ); diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index bfd4cd3584..aaf6fcb61a 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -608,7 +608,20 @@ 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]; + _insertedSubnodes = [self _filterLayouts:newLayout.sublayouts withIndexes:insertions]; + _deletedSubnodes = [self _filterLayouts:newLayout.sublayouts withIndexes:deletions]; + } else { + NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [newLayout.sublayouts count])]; + _insertedSubnodes = [self _filterLayouts:newLayout.sublayouts withIndexes:indexes]; + _deletedSubnodes = @[]; + } + + _layout = newLayout; _constrainedSize = constrainedSize; _flags.isMeasured = YES; [self calculatedLayoutDidChange]; @@ -628,6 +641,17 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return _layout; } +- (NSArray *)_filterLayouts:(NSArray *)layouts withIndexes:(NSIndexSet *)indexes +{ + NSMutableArray *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:node]; + }]; + return result; +} + - (void)calculatedLayoutDidChange { // subclass override @@ -1998,7 +2022,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], @"Cached 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"); @@ -2024,6 +2050,10 @@ static BOOL ShouldUseNewRenderingRange = YES; subnode = ((ASDisplayNode *)subnodeLayout.layoutableObject); [subnode setFrame:subnodeFrame]; } + + for (ASDisplayNode *node in _insertedSubnodes) { + [self addSubnode:node]; + } } - (void)displayWillStart diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 4b72f2c097..6d3704f929 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -59,6 +59,8 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) ASSizeRange _constrainedSize; UIEdgeInsets _hitTestSlop; NSMutableArray *_subnodes; + NSArray *_insertedSubnodes; + NSArray *_deletedSubnodes; ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; @@ -131,8 +133,11 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) - (BOOL)__shouldLoadViewOrLayer; - (BOOL)__shouldSize; -// Invoked by a call to setNeedsLayout to the underlying view +/** + Invoked by a call to setNeedsLayout to the underlying view + */ - (void)__setNeedsLayout; + - (void)__layout; - (void)__setSupernode:(ASDisplayNode *)supernode; diff --git a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m new file mode 100644 index 0000000000..71f318e771 --- /dev/null +++ b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m @@ -0,0 +1,66 @@ +// +// 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" + +@interface ASSpecTestDisplayNode : ASDisplayNode + +@property (copy, nonatomic) ASLayoutSpec * (^layoutSpecBlock)(ASSizeRange constrainedSize); + +@end + +@implementation ASSpecTestDisplayNode + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + return self.layoutSpecBlock(constrainedSize); +} + +@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)testInitialNodeInsertion +{ + ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; + ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; + ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init]; + node.layoutSpecBlock = ^(ASSizeRange constrainedSize){ + return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1, node2]]; + }; + [node measureWithSizeRange:ASSizeRangeMake(CGSizeZero, CGSizeZero)]; + [node layout]; // Layout immediately + XCTAssertEqual(node.subnodes[0], node1); + XCTAssertEqual(node.subnodes[1], node2); +} + +@end From bd1de07c77e22531eef19ef4e6cd7470dbc581f3 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Mon, 1 Feb 2016 15:31:46 -0800 Subject: [PATCH 11/13] Add custom comparision block to array diffing category --- AsyncDisplayKit/Private/NSArray+Diffing.h | 13 ++++++++++++- AsyncDisplayKit/Private/NSArray+Diffing.m | 15 +++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/AsyncDisplayKit/Private/NSArray+Diffing.h b/AsyncDisplayKit/Private/NSArray+Diffing.h index 374096f92f..a549d45f49 100644 --- a/AsyncDisplayKit/Private/NSArray+Diffing.h +++ b/AsyncDisplayKit/Private/NSArray+Diffing.h @@ -11,8 +11,19 @@ @interface NSArray (Diffing) /** - * Uses a bottom-up memoized longest common subsequence solution to identify differences. Runs in O(mn) complexity. + * @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 index 0cd9ad040b..00893d1416 100644 --- a/AsyncDisplayKit/Private/NSArray+Diffing.m +++ b/AsyncDisplayKit/Private/NSArray+Diffing.m @@ -12,13 +12,20 @@ - (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions { - NSIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array]; + [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 && [commonObjects[i] isEqual:array[j]]) { + if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) { i++; j++; } else { [insertionIndexes addIndex:j]; @@ -39,7 +46,7 @@ } } -- (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array +- (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--) { @@ -56,7 +63,7 @@ NSMutableIndexSet *common = [NSMutableIndexSet indexSet]; for (NSInteger i = 0, j = 0; i < self.count && j < array.count;) { - if ([self[i] isEqual:array[j]]) { + if (comparison(self[i], array[j])) { [common addIndex:i]; i++; j++; } else if (lengths[i+1][j] >= lengths[i][j+1]) { From d168ec78ceddc4e72ac2b5c4fe2c7fcb79fc023e Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Mon, 1 Feb 2016 17:49:03 -0800 Subject: [PATCH 12/13] Implement simple, in-order add/remove subnode support when changing layout specs --- AsyncDisplayKit/ASDisplayNode.mm | 81 ++++++++++++++++--- .../Private/ASDisplayNodeInternal.h | 9 ++- .../ASDisplayNodeImplicitHierarchyTests.m | 73 +++++++++++++++-- 3 files changed, 145 insertions(+), 18 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index aaf6fcb61a..8fb345bb5f 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -30,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 () /** @@ -609,18 +639,20 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // - the constrained size range is different if (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)) { ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize]; - + if (_layout) { NSIndexSet *insertions, *deletions; - [_layout.sublayouts asdk_diffWithArray:newLayout.sublayouts insertions:&insertions deletions:&deletions]; - _insertedSubnodes = [self _filterLayouts:newLayout.sublayouts withIndexes:insertions]; - _deletedSubnodes = [self _filterLayouts:newLayout.sublayouts withIndexes: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 _filterSublayouts:newLayout.sublayouts withIndexes:insertions]; + _deletedSubnodes = [self _filterSublayouts:_layout.sublayouts withIndexes:deletions]; } else { NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [newLayout.sublayouts count])]; - _insertedSubnodes = [self _filterLayouts:newLayout.sublayouts withIndexes:indexes]; + _insertedSubnodes = [self _filterSublayouts:newLayout.sublayouts withIndexes:indexes]; _deletedSubnodes = @[]; } - + _layout = newLayout; _constrainedSize = constrainedSize; _flags.isMeasured = YES; @@ -641,13 +673,13 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return _layout; } -- (NSArray *)_filterLayouts:(NSArray *)layouts withIndexes:(NSIndexSet *)indexes +- (NSArray<_ASDisplayNodePosition *> *)_filterSublayouts:(NSArray *)layouts withIndexes:(NSIndexSet *)indexes { - NSMutableArray *result = [NSMutableArray array]; + 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:node]; + [result addObject:[_ASDisplayNodePosition positionWithNode:node atIndex:idx]]; }]; return result; } @@ -2051,11 +2083,38 @@ static BOOL ShouldUseNewRenderingRange = YES; [subnode setFrame:subnodeFrame]; } - for (ASDisplayNode *node in _insertedSubnodes) { - [self addSubnode:node]; + if ([[self class] usesImplicitHierarchyManagement]) { + if (!_managedSubnodes) { + _managedSubnodes = [NSMutableArray array]; + } + + 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 +{ + 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 +{ + [_managedSubnodes removeObjectAtIndex:idx]; + [node removeFromSupernode]; +} + - (void)displayWillStart { // in case current node takes longer to display than it's subnodes, treat it as a dependent node diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 6d3704f929..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 @@ -59,8 +60,12 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) ASSizeRange _constrainedSize; UIEdgeInsets _hitTestSlop; NSMutableArray *_subnodes; - NSArray *_insertedSubnodes; - NSArray *_deletedSubnodes; + + // Subnodes implicitly managed by layout changes + NSMutableArray *_managedSubnodes; + + NSArray<_ASDisplayNodePosition *> *_insertedSubnodes; + NSArray<_ASDisplayNodePosition *> *_deletedSubnodes; ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; diff --git a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m index 71f318e771..c565297d7c 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeImplicitHierarchyTests.m @@ -11,19 +11,35 @@ #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); +@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); + return self.layoutSpecBlock(constrainedSize, _layoutState); } @end @@ -49,18 +65,65 @@ XCTAssert([ASDisplayNode usesImplicitHierarchyManagement]); } -- (void)testInitialNodeInsertion +- (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){ - return [ASStaticLayoutSpec staticLayoutSpecWithChildren:@[node1, node2]]; + 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 From ac3c9d220beb89866fe91dd15d69ffb7be65a7b5 Mon Sep 17 00:00:00 2001 From: Levi McCallum Date: Mon, 1 Feb 2016 18:29:50 -0800 Subject: [PATCH 13/13] Respond to review comments --- AsyncDisplayKit/ASDisplayNode.mm | 48 +++++++++++++++++--------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 8fb345bb5f..18753cf85c 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -98,7 +98,8 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; static BOOL usesImplicitHierarchyManagement = FALSE; -+ (BOOL)usesImplicitHierarchyManagement { ++ (BOOL)usesImplicitHierarchyManagement +{ return usesImplicitHierarchyManagement; } @@ -645,11 +646,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [_layout.sublayouts asdk_diffWithArray:newLayout.sublayouts insertions:&insertions deletions:&deletions compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) { return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject); }]; - _insertedSubnodes = [self _filterSublayouts:newLayout.sublayouts withIndexes:insertions]; - _deletedSubnodes = [self _filterSublayouts:_layout.sublayouts withIndexes:deletions]; + _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 _filterSublayouts:newLayout.sublayouts withIndexes:indexes]; + _insertedSubnodes = [self _filterNodesInLayouts:newLayout.sublayouts withIndexes:indexes]; _deletedSubnodes = @[]; } @@ -667,13 +668,19 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // 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) { - [self __generatePlaceholder]; + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + + if (_placeholderLayer) { + [self _setupPlaceholderLayerContents]; + } } return _layout; } -- (NSArray<_ASDisplayNodePosition *> *)_filterSublayouts:(NSArray *)layouts withIndexes:(NSIndexSet *)indexes +- (NSArray<_ASDisplayNodePosition *> *)_filterNodesInLayouts:(NSArray *)layouts withIndexes:(NSIndexSet *)indexes { NSMutableArray<_ASDisplayNodePosition *> *result = [NSMutableArray array]; [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { @@ -689,17 +696,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // subclass override } -- (void)__generatePlaceholder -{ - if (!_placeholderImage) { - _placeholderImage = [self placeholderImage]; - } - - if (_placeholderLayer) { - [self _setupPlaceholderLayerContents]; - } -} - - (BOOL)displaysAsynchronously { ASDN::MutexLocker l(_propertyLock); @@ -2055,7 +2051,7 @@ static BOOL ShouldUseNewRenderingRange = YES; CGRect subnodeFrame = CGRectZero; for (ASLayout *subnodeLayout in _layout.sublayouts) { if (![[self class] usesImplicitHierarchyManagement]) { - ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Cached sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes); + ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes); } CGPoint adjustedOrigin = subnodeLayout.position; if (isfinite(adjustedOrigin.x) == NO) { @@ -2084,10 +2080,6 @@ static BOOL ShouldUseNewRenderingRange = YES; } if ([[self class] usesImplicitHierarchyManagement]) { - if (!_managedSubnodes) { - _managedSubnodes = [NSMutableArray array]; - } - for (_ASDisplayNodePosition *position in _deletedSubnodes) { [self _implicitlyRemoveSubnode:position.node atIndex:position.index]; } @@ -2100,6 +2092,12 @@ static BOOL ShouldUseNewRenderingRange = YES; - (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]; @@ -2111,6 +2109,12 @@ static BOOL ShouldUseNewRenderingRange = YES; - (void)_implicitlyRemoveSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx { + ASDisplayNodeAssertThreadAffinity(self); + + if (!_managedSubnodes) { + _managedSubnodes = [NSMutableArray array]; + } + [_managedSubnodes removeObjectAtIndex:idx]; [node removeFromSupernode]; }