diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index bb8227f27..4ce8706e6 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 058D0A40195D057000B7D73C /* ASTextNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A36195D057000B7D73C /* ASTextNodeTests.m */; }; 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */; }; 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */; }; + 0FAFDF7520EC1C90003A51C0 /* ASLayout+IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0FAFDF7620EC1C90003A51C0 /* ASLayout+IGListKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */; }; 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */; }; 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -108,7 +110,7 @@ 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.m */; }; 509E68651B3AEDC5009B9150 /* CoreGraphics+ASConvenience.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */; settings = {ATTRIBUTES = (Public, ); }; }; 509E68661B3AEDD7009B9150 /* CoreGraphics+ASConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.m */; }; - 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */; }; + 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */ = {isa = PBXBuildFile; fileRef = DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */; }; 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */ = {isa = PBXBuildFile; fileRef = AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */; }; 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = 68FC85DC1CE29AB700EDD713 /* ASNavigationController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */; }; @@ -601,6 +603,8 @@ 058D0A44195D058D00B7D73C /* ASBaseDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBaseDefines.h; sourceTree = ""; }; 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASSnapshotTestCase.m; sourceTree = ""; }; 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; + 0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASLayout+IGListKit.h"; sourceTree = ""; }; + 0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayout+IGListKit.mm"; sourceTree = ""; }; 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = ""; }; 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionNode.mm; sourceTree = ""; }; 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; @@ -964,7 +968,7 @@ DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = _ASTransitionContext.m; path = ../_ASTransitionContext.m; sourceTree = ""; }; DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASContextTransitioning.h; 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 = ""; }; + DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSArray+Diffing.mm"; sourceTree = ""; }; DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ArrayDiffingTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASDisplayNodeImplicitHierarchyTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPagerFlowLayout.h; sourceTree = ""; }; @@ -1416,7 +1420,7 @@ 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */, 205F0E201B376416007741D0 /* CoreGraphics+ASConvenience.m */, DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */, - DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */, + DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */, CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */, CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */, 058D09F5195D050800B7D73C /* NSMutableAttributedString+TextKitAdditions.h */, @@ -1635,6 +1639,8 @@ ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */, ACF6ED0B1B17843500DA7C62 /* ASLayout.h */, ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */, + 0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */, + 0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */, ACF6ED111B17843500DA7C62 /* ASLayoutElement.h */, E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */, 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */, @@ -1850,6 +1856,7 @@ 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */, 698C8B621CAB49FC0052DC3F /* ASLayoutElementExtensibility.h in Headers */, 69F10C871C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h in Headers */, + 0FAFDF7520EC1C90003A51C0 /* ASLayout+IGListKit.h in Headers */, B350623C1B010EFD0018CF92 /* _ASAsyncTransaction.h in Headers */, 68355B411CB57A6C001D4E68 /* ASImageContainerProtocolCategories.h in Headers */, 7630FFA81C9E267E007A7C0E /* ASVideoNode.h in Headers */, @@ -2368,6 +2375,7 @@ E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */, 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.m in Sources */, + 0FAFDF7620EC1C90003A51C0 /* ASLayout+IGListKit.mm in Sources */, CCDC9B4E200991D10063C1F8 /* ASGraphicsContext.m in Sources */, CCCCCCD81EC3EF060087FE10 /* ASTextInput.m in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, @@ -2402,7 +2410,7 @@ B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m in Sources */, 25E327591C16819500A2170C /* ASPagerNode.m in Sources */, - 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */, + 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */, B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, DEC146B91C37A16A004A0EE7 /* ASCollectionInternal.m in Sources */, 254C6B891BF94F8A003EC431 /* ASTextKitRenderer+Positioning.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index e47a52ecc..b8b779814 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [ASLayoutTransition] Add support for preserving order after node moves during transitions. (This order defines the z-order as well.) [Kevin Smith](https://github.com/wiseoldduck) [#1006] - [ASDisplayNode] Adds support for multiple interface state delegates. [Garrett Moon](https://github.com/garrettmoon) [#979](https://github.com/TextureGroup/Texture/pull/979) - [ASDataController] Add capability to renew supplementary views (update map) when size change from zero to non-zero.[Max Wang](https://github.com/wsdwsd0829) [#842](https://github.com/TextureGroup/Texture/pull/842) - Make `ASPerformMainThreadDeallocation` visible in C. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 239057e33..8037cbf1b 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -680,7 +680,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize } // Apply the subnode insertion immediately to be able to animate the nodes - [pendingLayoutTransition applySubnodeInsertions]; + [pendingLayoutTransition applySubnodeInsertionsAndMoves]; // Kick off animating the layout transition { diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 227b13f21..d98d00361 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -137,3 +137,4 @@ #import #import +#import diff --git a/Source/Details/NSArray+Diffing.h b/Source/Details/NSArray+Diffing.h index 3a1729337..3fd83806e 100644 --- a/Source/Details/NSArray+Diffing.h +++ b/Source/Details/NSArray+Diffing.h @@ -17,6 +17,60 @@ #import +/** + * These changes can be used to transform `self` to `array` by applying them in (any) order, *without shifting* the + * other elements. This can be done (in an NSMutableArray) by calling `setObject:atIndexedSubscript:` (or just use + * [subscripting] directly) for insertions from `array` into `self` (not the seemingly more apt `insertObject:atIndex`!), + * and using the same method for deletions from `self` (*set* a `[NSNull null]` as opposed to `removeObject:atIndex:`). + * After all inserts/deletes have been applied, there will be no nulls left (except possibly at the end of the array if + * `[array count] < [self count]`) + + * Some examples: + * in: ab c + * out: abdc + * diff: ..+. + * + * in: abcd + * out: dcba + * dif: ---.+++ + * + * in: abcd + * out: ab d + * diff: ..-. + * + * in: a bcd + * out: adbc + * diff: .+..- + * + * If `moves` pointer is passed in, instances where one element moves to another location are detected and reported, + * possibly replacing pairs of delete/insert. The process for transforming an array remains the same, however now it is + * important to apply the moves in order and not overwrite an element that needs to be moved somewhere else. + * + * the same examples, with moves: + * in: ab c + * out: abdc + * diff: ..+. + * + * in: abcd + * out: dcba + * diff: 321. + * + * in: abcd + * out: ab d + * diff: ..-. + * + * in: abcd + * out: adbc + * diff: .312 + * + * Other notes: + * + * No index will be both moved from and deleted. + * Each index 0...[self count] will be either moved from or deleted. If it is moved to the same location, we omit it. + * Each index 0...[array count] will be the destination of ONE move or ONE insert. + * Knowing these things means any two of the three (delete, move, insert) implies the third. + */ + @interface NSArray (Diffing) /** @@ -35,4 +89,12 @@ */ - (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison; +/** + * @abstract Compares two arrays, providing the insertion, deletion, and move 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. + * The moves are returned in ascending order of their destination index. + */ +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions moves:(NSArray **)moves; @end diff --git a/Source/Details/NSArray+Diffing.m b/Source/Details/NSArray+Diffing.m deleted file mode 100644 index ee0ba0ff3..000000000 --- a/Source/Details/NSArray+Diffing.m +++ /dev/null @@ -1,113 +0,0 @@ -// -// NSArray+Diffing.m -// Texture -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional -// grant of patent rights can be found in the PATENTS file in the same directory. -// -// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, -// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// - -#import -#import - -@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 -{ - NSAssert(comparison != nil, @"Comparison block is required"); - NSIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison]; - - if (insertions) { - NSArray *commonObjects = [self objectsAtIndexes:commonIndexes]; - NSMutableIndexSet *insertionIndexes = [[NSMutableIndexSet alloc] init]; - 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 alloc] init]; - 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 -{ - NSAssert(comparison != nil, @"Comparison block is required"); - - NSInteger selfCount = self.count; - NSInteger arrayCount = array.count; - - // Allocate the diff map in the heap so we don't blow the stack for large arrays. - NSInteger **lengths = NULL; - lengths = (NSInteger **)malloc(sizeof(NSInteger*) * (selfCount+1)); - if (lengths == NULL) { - ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); - return nil; - } - - for (NSInteger i = 0; i <= selfCount; i++) { - lengths[i] = (NSInteger *)malloc(sizeof(NSInteger) * (arrayCount+1)); - if (lengths[i] == NULL) { - ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); - return nil; - } - id selfObj = i > 0 ? self[i-1] : nil; - for (NSInteger j = 0; j <= arrayCount; j++) { - if (i == 0 || j == 0) { - lengths[i][j] = 0; - } else if (comparison(selfObj, array[j-1])) { - 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 alloc] init]; - NSInteger i = selfCount, j = arrayCount; - while(i > 0 && j > 0) { - if (comparison(self[i-1], array[j-1])) { - [common addIndex:(i-1)]; - i--; j--; - } else if (lengths[i-1][j] > lengths[i][j-1]) { - i--; - } else { - j--; - } - } - - for (NSInteger i = 0; i <= selfCount; i++) { - free(lengths[i]); - } - free(lengths); - return common; -} - -@end diff --git a/Source/Details/NSArray+Diffing.mm b/Source/Details/NSArray+Diffing.mm new file mode 100644 index 000000000..64063ff71 --- /dev/null +++ b/Source/Details/NSArray+Diffing.mm @@ -0,0 +1,185 @@ +// +// NSArray+Diffing.m +// Texture +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@implementation NSArray (Diffing) + +typedef BOOL (^compareBlock)(id _Nonnull lhs, id _Nonnull rhs); + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:[NSArray defaultCompareBlock]]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + compareBlock:(compareBlock)comparison +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:comparison]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + moves:(NSArray **)moves +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:moves + compareBlock:[NSArray defaultCompareBlock]]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + moves:(NSArray **)moves compareBlock:(compareBlock)comparison +{ + struct NSObjectHash + { + std::size_t operator()(id k) const { return (std::size_t) [k hash]; }; + }; + struct NSObjectCompare + { + bool operator()(id lhs, id rhs) const { return (bool) [lhs isEqual:rhs]; }; + }; + std::unordered_multimap potentialMoves; + + NSAssert(comparison != nil, @"Comparison block is required"); + NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)"); + NSMutableArray *moveIndexPaths = nil; + NSMutableIndexSet *insertionIndexes = nil, *deletionIndexes = nil; + if (moves) { + moveIndexPaths = [NSMutableArray new]; + } + NSMutableIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison]; + + if (deletions || moves) { + deletionIndexes = [NSMutableIndexSet indexSet]; + NSUInteger i = 0; + for (id element in self) { + if (![commonIndexes containsIndex:i]) { + [deletionIndexes addIndex:i]; + } + if (moves) { + potentialMoves.insert(std::pair(element, i)); + } + ++i; + } + } + + if (insertions || moves) { + insertionIndexes = [NSMutableIndexSet indexSet]; + NSArray *commonObjects = [self objectsAtIndexes:commonIndexes]; + for (NSUInteger i = 0, j = 0; j < array.count; j++) { + auto moveFound = potentialMoves.find(array[j]); + NSUInteger movedFrom = NSNotFound; + if (moveFound != potentialMoves.end() && moveFound->second != j) { + movedFrom = moveFound->second; + potentialMoves.erase(moveFound); + [moveIndexPaths addObject:[NSIndexPath indexPathForItem:j inSection:movedFrom]]; + } + if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) { + i++; + } else { + if (movedFrom != NSNotFound) { + // moves will coalesce a delete / insert - the insert is just not done, and here we remove the delete: + [deletionIndexes removeIndex:movedFrom]; + // OR a move will have come from the LCS: + if ([commonIndexes containsIndex:movedFrom]) { + [commonIndexes removeIndex:movedFrom]; + commonObjects = [self objectsAtIndexes:commonIndexes]; + } + } else { + [insertionIndexes addIndex:j]; + } + } + } + } + + if (moves) {*moves = moveIndexPaths;} + if (deletions) {*deletions = deletionIndexes;} + if (insertions) {*insertions = insertionIndexes;} +} + +// https://github.com/raywenderlich/swift-algorithm-club/tree/master/Longest%20Common%20Subsequence is not exactly this code (obviously), but +// is a good commentary on the algorithm. +- (NSMutableIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison +{ + NSAssert(comparison != nil, @"Comparison block is required"); + + NSInteger selfCount = self.count; + NSInteger arrayCount = array.count; + + // Allocate the diff map in the heap so we don't blow the stack for large arrays. + NSInteger **lengths = NULL; + lengths = (NSInteger **)malloc(sizeof(NSInteger*) * (selfCount+1)); + if (lengths == NULL) { + ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); + return nil; + } + // Fill in a LCS length matrix: + for (NSInteger i = 0; i <= selfCount; i++) { + lengths[i] = (NSInteger *)malloc(sizeof(NSInteger) * (arrayCount+1)); + if (lengths[i] == NULL) { + ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); + return nil; + } + id selfObj = i > 0 ? self[i-1] : nil; + for (NSInteger j = 0; j <= arrayCount; j++) { + if (i == 0 || j == 0) { + lengths[i][j] = 0; + } else if (comparison(selfObj, array[j-1])) { + lengths[i][j] = 1 + lengths[i-1][j-1]; + } else { + lengths[i][j] = MAX(lengths[i-1][j], lengths[i][j-1]); + } + } + } + // Backtrack to fill in indices based on length matrix: + NSMutableIndexSet *common = [NSMutableIndexSet indexSet]; + NSInteger i = selfCount, j = arrayCount; + while(i > 0 && j > 0) { + if (comparison(self[i-1], array[j-1])) { + [common addIndex:(i-1)]; + i--; j--; + } else if (lengths[i-1][j] > lengths[i][j-1]) { + i--; + } else { + j--; + } + } + + for (NSInteger i = 0; i <= selfCount; i++) { + free(lengths[i]); + } + free(lengths); + return common; +} + +static compareBlock defaultCompare = nil; + ++ (compareBlock)defaultCompareBlock +{ + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + defaultCompare = ^BOOL(id lhs, id rhs) { + return [lhs isEqual:rhs]; + }; + }); + + return defaultCompare; +} + +@end diff --git a/Source/Layout/ASLayout+IGListKit.h b/Source/Layout/ASLayout+IGListKit.h new file mode 100644 index 000000000..65c45a029 --- /dev/null +++ b/Source/Layout/ASLayout+IGListKit.h @@ -0,0 +1,19 @@ +// +// ASLayout+IGListKit.mm +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#if AS_IG_LIST_KIT +#import +#import +@interface ASLayout(IGListKit) +@end + +#endif // AS_IG_LIST_KIT diff --git a/Source/Layout/ASLayout+IGListKit.mm b/Source/Layout/ASLayout+IGListKit.mm new file mode 100644 index 000000000..438689153 --- /dev/null +++ b/Source/Layout/ASLayout+IGListKit.mm @@ -0,0 +1,34 @@ +// +// ASLayout+IGListKit.mm +// Texture +// +// Copyright (c) 2018-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +#import +#if AS_IG_LIST_KIT +#import "ASLayout+IGListKit.h" + +@interface ASLayout() { +@public + id _layoutElement; +} +@end + +@implementation ASLayout(IGListKit) + +- (id )diffIdentifier +{ + return self->_layoutElement; +} + +- (BOOL)isEqualToDiffableObject:(id )other +{ + return [self isEqual:other]; +} +@end +#endif // AS_IG_LIST_KIT diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 2007c28da..7e47ea027 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -283,6 +283,8 @@ - (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED - (BOOL)isEqual:(id)object { + if (self == object) return YES; + ASLayout *layout = ASDynamicCast(object, ASLayout); if (layout == nil) { return NO; diff --git a/Source/Private/ASLayoutTransition.h b/Source/Private/ASLayoutTransition.h index a34dd2e72..1060a57d8 100644 --- a/Source/Private/ASLayoutTransition.h +++ b/Source/Private/ASLayoutTransition.h @@ -85,9 +85,10 @@ AS_SUBCLASSING_RESTRICTED - (void)commitTransition; /** - * Insert all new subnodes that were added between the previous layout and the pending layout + * Insert all new subnodes that were added and move the subnodes that moved between the previous layout and + * the pending layout. */ -- (void)applySubnodeInsertions; +- (void)applySubnodeInsertionsAndMoves; /** * Remove all subnodes that are removed between the previous layout and the pending layout diff --git a/Source/Private/ASLayoutTransition.mm b/Source/Private/ASLayoutTransition.mm index f1b15dd02..77a8fdd7c 100644 --- a/Source/Private/ASLayoutTransition.mm +++ b/Source/Private/ASLayoutTransition.mm @@ -25,10 +25,11 @@ #import #import -#import -#import -#import +#if AS_IG_LIST_KIT +#import +#import +#endif /** * Search the whole layout stack if at least one layout has a layoutElement object that can not be layed out asynchronous. @@ -66,7 +67,7 @@ @implementation ASLayoutTransition { NSArray *_insertedSubnodes; NSArray *_removedSubnodes; std::vector _insertedSubnodePositions; - std::vector _removedSubnodePositions; + std::vector> _subnodeMoves; } - (instancetype)initWithNode:(ASDisplayNode *)node @@ -98,27 +99,44 @@ - (BOOL)isSynchronous - (void)commitTransition { - [self applySubnodeInsertions]; [self applySubnodeRemovals]; + [self applySubnodeInsertionsAndMoves]; } -- (void)applySubnodeInsertions +- (void)applySubnodeInsertionsAndMoves { ASDN::MutexSharedLocker l(__instanceLock__); [self calculateSubnodeOperationsIfNeeded]; // Create an activity even if no subnodes affected. - as_activity_create_for_scope("Apply subnode insertions"); - if (_insertedSubnodes.count == 0) { + as_activity_create_for_scope("Apply subnode insertions and moves"); + if (_insertedSubnodePositions.size() == 0 && _subnodeMoves.size() == 0) { return; } ASDisplayNodeLogEvent(_node, @"insertSubnodes: %@", _insertedSubnodes); NSUInteger i = 0; - for (ASDisplayNode *node in _insertedSubnodes) { + NSUInteger j = 0; + for (auto const &move : _subnodeMoves) { + [move.first _removeFromSupernodeIfEqualTo:_node]; + } + j = 0; + while (i < _insertedSubnodePositions.size() && j < _subnodeMoves.size()) { NSUInteger p = _insertedSubnodePositions[i]; - [_node _insertSubnode:node atIndex:p]; - i += 1; + NSUInteger q = _subnodeMoves[j].second; + if (p < q) { + [_node _insertSubnode:_insertedSubnodes[i] atIndex:p]; + i++; + } else { + [_node _insertSubnode:_subnodeMoves[j].first atIndex:q]; + j++; + } + } + for (; i < _insertedSubnodePositions.size(); ++i) { + [_node _insertSubnode:_insertedSubnodes[i] atIndex:_insertedSubnodePositions[i]]; + } + for (; j < _subnodeMoves.size(); ++j) { + [_node _insertSubnode:_subnodeMoves[j].first atIndex:_subnodeMoves[j].second]; } } @@ -156,23 +174,42 @@ - (void)calculateSubnodeOperationsIfNeeded ASLayout *pendingLayout = _pendingLayout->layout; if (previousLayout) { +#if AS_IG_LIST_KIT + // IGListDiff completes in linear time O(m+n), so use it if we have it: + IGListIndexSetResult *result = IGListDiff(previousLayout.sublayouts, pendingLayout.sublayouts, IGListDiffEquality); + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, result.inserts, &_insertedSubnodes); + findNodesInLayoutAtIndexes(previousLayout, result.deletes, &_removedSubnodes); + for (IGListMoveIndex *move in result.moves) { + _subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[move.from].layoutElement, move.to)); + } + + // Sort by ascending order of move destinations, this will allow easy loop of `insertSubnode:AtIndex` later. + std::sort(_subnodeMoves.begin(), _subnodeMoves.end(), [](std::pair, NSUInteger> a, + std::pair b) { + return a.second < b.second; + }); +#else NSIndexSet *insertions, *deletions; - [previousLayout.sublayouts asdk_diffWithArray:pendingLayout.sublayouts + NSArray *moves; + NSArray *previousNodes = [previousLayout.sublayouts valueForKey:@"layoutElement"]; + NSArray *pendingNodes = [pendingLayout.sublayouts valueForKey:@"layoutElement"]; + [previousNodes asdk_diffWithArray:pendingNodes insertions:&insertions deletions:&deletions - compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) { - return ASObjectIsEqual(lhs.layoutElement, rhs.layoutElement); - }]; + moves:&moves]; + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, insertions, &_insertedSubnodes); - _removedSubnodePositions = findNodesInLayoutAtIndexesWithFilteredNodes(previousLayout, - deletions, - _insertedSubnodes, - &_removedSubnodes); + _removedSubnodes = [previousNodes objectsAtIndexes:deletions]; + // These should arrive sorted in ascending order of move destinations. + for (NSIndexPath *move in moves) { + _subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[([move indexAtPosition:0])].layoutElement, + [move indexAtPosition:1])); + } +#endif } else { NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [pendingLayout.sublayouts count])]; _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, indexes, &_insertedSubnodes); _removedSubnodes = nil; - _removedSubnodePositions.clear(); } _calculatedSubnodeOperations = YES; } @@ -254,7 +291,7 @@ - (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSize for (ASLayout *sublayout in layout.sublayouts) { if (idx > lastIndex) { break; } if (idx >= firstIndex && [indexes containsIndex:idx]) { - ASDisplayNode *node = (ASDisplayNode *)sublayout.layoutElement; + ASDisplayNode *node = (ASDisplayNode *) sublayout.layoutElement; ASDisplayNodeCAssert(node, @"ASDisplayNode was deallocated before it was added to a subnode. It's likely the case that you use automatically manages subnodes and allocate a ASDisplayNode in layoutSpecThatFits: and don't have any strong reference to it."); // Ignore the odd case in which a non-node sublayout is accessed and the type cast fails if (node != nil) { diff --git a/Tests/ASDisplayNodeImplicitHierarchyTests.m b/Tests/ASDisplayNodeImplicitHierarchyTests.m index 383d5cfd9..5d305ceb8 100644 --- a/Tests/ASDisplayNodeImplicitHierarchyTests.m +++ b/Tests/ASDisplayNodeImplicitHierarchyTests.m @@ -150,7 +150,11 @@ - (void)testCalculatedLayoutHierarchyTransitions ASDisplayNode *node1 = [[ASDisplayNode alloc] init]; ASDisplayNode *node2 = [[ASDisplayNode alloc] init]; ASDisplayNode *node3 = [[ASDisplayNode alloc] init]; - + + node1.debugName = @"a"; + node2.debugName = @"b"; + node3.debugName = @"c"; + // As we will involve a stack spec we have to give the nodes an intrinsic content size node1.style.preferredSize = kSize; node2.style.preferredSize = kSize; diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index 989997007..3c6d9a71e 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -2392,6 +2392,77 @@ - (void)testThatHavingTheSameNodeTwiceInALayoutSpecCausesExceptionOnLayoutCalcul XCTAssertThrowsSpecificNamed([node calculateLayoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))], NSException, NSInternalInconsistencyException); } +- (void)testThatStackSpecOrdersSubnodesCorrectlyRandomness +{ + // This test ensures that the z-order of nodes matches the stack spec, including after several random relayouts / transitions. + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + + DeclareNodeNamed(a); + DeclareNodeNamed(b); + DeclareNodeNamed(c); + DeclareNodeNamed(d); + DeclareNodeNamed(e); + DeclareNodeNamed(f); + DeclareNodeNamed(g); + DeclareNodeNamed(h); + DeclareNodeNamed(i); + DeclareNodeNamed(j); + + NSMutableArray *allNodes = [@[a, b, c, d, e, f, g, h, i, j] mutableCopy]; + NSArray *testPrevious = @[]; + NSArray __block *testPending = @[]; + + int len1 = 1 + arc4random_uniform(9); + for (NSUInteger n = 0; n < len1; n++) { // shuffle and add + [allNodes exchangeObjectAtIndex:n withObjectAtIndex:n + arc4random_uniform(10 - (uint32_t) n)]; + testPrevious = [testPrevious arrayByAddingObject:allNodes[n]]; + } + + __block NSUInteger testCount = 0; + node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { + ASStackLayoutSpec *stack = [ASStackLayoutSpec verticalStackLayoutSpec]; + + if (testCount++ == 0) { + stack.children = testPrevious; + } + else { + testPending = @[]; + int len2 = 1 + arc4random_uniform(9); + for (NSUInteger n = 0; n < len2; n++) { // shuffle and add + [allNodes exchangeObjectAtIndex:n withObjectAtIndex:n + arc4random_uniform(10 - (uint32_t) n)]; + testPending = [testPending arrayByAddingObject:allNodes[n]]; + } + stack.children = testPending; + } + + return stack; + }; + + ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100)); + [node.view layoutIfNeeded]; + + // Because automaticallyManagesSubnodes is used, the subnodes array is constructed from the layout spec's children. + NSString *expected = [[testPrevious valueForKey:@"debugName"] componentsJoinedByString:@","]; + XCTAssert([node.subnodes isEqualToArray:testPrevious], @"subnodes: %@, array: %@", node.subnodes, testPrevious); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + expected, @"Initial order"); + + for (NSUInteger n = 0; n < 25; n++) { + [node invalidateCalculatedLayout]; + [node.view setNeedsLayout]; + [node.view layoutIfNeeded]; + + + XCTAssert([node.subnodes isEqualToArray:testPending], @"subnodes: %@, array: %@", node.subnodes, testPending); + expected = [[testPending valueForKey:@"debugName"] componentsJoinedByString:@","]; + + XCTAssertEqualObjects(orderStringFromSubnodes(node), expected, @"Incorrect node order for Random order #%ld", (unsigned long) n); + XCTAssertEqualObjects(orderStringFromSubviews(node.view), expected, @"Incorrect subviews for Random order #%ld", (unsigned long) n); + XCTAssertEqualObjects(orderStringFromSublayers(node.layer), expected, @"Incorrect sublayers for Random order #%ld", (unsigned long) n); + } +} + - (void)testThatStackSpecOrdersSubnodesCorrectly { // This test ensures that the z-order of nodes matches the stack spec, including after relayout / transition. @@ -2402,14 +2473,26 @@ - (void)testThatStackSpecOrdersSubnodesCorrectly DeclareNodeNamed(b); DeclareNodeNamed(c); DeclareNodeNamed(d); + DeclareNodeNamed(e); NSArray *nodesForwardOrder = @[a, b, c, d]; NSArray *nodesReverseOrder = @[d, c, b, a]; + NSArray *addAndMoveOrder = @[a, b, e, d, c]; __block BOOL flipItemOrder = NO; + __block NSUInteger testCount = 0; node.layoutSpecBlock = ^(ASDisplayNode *node, ASSizeRange size) { ASStackLayoutSpec *stack = [ASStackLayoutSpec verticalStackLayoutSpec]; - stack.children = flipItemOrder ? nodesReverseOrder : nodesForwardOrder; + switch(testCount) { + case 0: + stack.children = nodesForwardOrder; break; + case 1: + stack.children = nodesReverseOrder; break; + case 2: + default: + stack.children = addAndMoveOrder; break; + } + testCount++; return stack; }; @@ -2423,13 +2506,22 @@ - (void)testThatStackSpecOrdersSubnodesCorrectly flipItemOrder = YES; [node invalidateCalculatedLayout]; + [node.view setNeedsLayout]; [node.view layoutIfNeeded]; // In this case, it's critical that the items are in the new order so that event handling and apparent z-position are correct. // FIXME: The reversal case is not currently passing. - // XCTAssert([node.subnodes isEqualToArray:nodesReverseOrder], @"subnodes: %@, array: %@", node.subnodes, nodesReverseOrder); - // XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, - // @"d,c,b,a", @"Reverse order"); + XCTAssert([node.subnodes isEqualToArray:nodesReverseOrder], @"subnodes: %@, array: %@", node.subnodes, nodesReverseOrder); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + @"d,c,b,a", @"Reverse order"); + + [node invalidateCalculatedLayout]; + [node.view setNeedsLayout]; + [node.view layoutIfNeeded]; + XCTAssert([node.subnodes isEqualToArray:addAndMoveOrder], @"subnodes: %@, array: %@", node.subnodes, addAndMoveOrder); + XCTAssertNodeSubnodeSubviewSublayerOrder(node, YES /* isLoaded */, NO /* isLayerBacked */, + @"a,b,e,d,c", @"AddAndMove order"); + } - (void)testThatOverlaySpecOrdersSubnodesCorrectly diff --git a/Tests/ArrayDiffingTests.m b/Tests/ArrayDiffingTests.m index 5d25bfde7..b9af5681a 100644 --- a/Tests/ArrayDiffingTests.m +++ b/Tests/ArrayDiffingTests.m @@ -127,7 +127,8 @@ - (void)testDiffingInsertionsAndDeletions { @[@0, @1, @2], ], ]; - + + long n = 0; for (NSArray *test in tests) { NSIndexSet *insertions, *deletions; [test[0] asdk_diffWithArray:test[1] insertions:&insertions deletions:&deletions]; @@ -135,17 +136,186 @@ - (void)testDiffingInsertionsAndDeletions { NSMutableIndexSet *mutableDeletions = [deletions mutableCopy]; for (NSNumber *index in (NSArray *)test[2]) { - XCTAssert([mutableInsertions containsIndex:[index integerValue]]); + XCTAssert([mutableInsertions containsIndex:[index integerValue]], @"Test #%ld: insertions %@ does not contain %@", + n, insertions, index); [mutableInsertions removeIndex:[index integerValue]]; } for (NSNumber *index in (NSArray *)test[3]) { - XCTAssert([mutableDeletions containsIndex:[index integerValue]]); + XCTAssert([mutableDeletions containsIndex:[index integerValue]], @"Test #%ld: deletions %@ does not contain %@", + n, deletions, index + ); [mutableDeletions removeIndex:[index integerValue]]; } - XCTAssert([mutableInsertions count] == 0, @"Unaccounted insertions: %@", mutableInsertions); - XCTAssert([mutableDeletions count] == 0, @"Unaccounted deletions: %@", mutableDeletions); + XCTAssert([mutableInsertions count] == 0, @"Test #%ld: Unaccounted insertions: %@", n, mutableInsertions); + XCTAssert([mutableDeletions count] == 0, @"Test #%ld: Unaccounted deletions: %@", n, mutableDeletions); + n++; + } +} + +- (void)testDiffingInsertsDeletesAndMoves +{ + NSArray *tests = @[ + @[ + @[@"a", @"b"], + @[@"b", @"a"], + @[], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]) {1, 0} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]) {0, 1} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"alice", @"dave", @"gary"], + @[@3], + @[], + @[]], + @[ + @[@"a", @"b", @"c", @"d"], + @[@"d", @"c", @"b", @"a"], + @[], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]){3, 0} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){2, 1} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){1, 2} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){0, 3} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave"], + @[@"bob", @"gary", @"dave", @"alice"], + @[@1], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]) {1, 3} length:2] + ]], + @[ + @[@"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], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]){0, 1} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){5, 3} length:2] + ]], + @[ + @[@"bob", @"alice", @"dave", @"judy"], + @[@"judy", @"dave", @"alice", @"bob"], + @[], + @[], + @[[NSIndexPath indexPathWithIndexes:(NSUInteger[]){3, 0} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){2, 1} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){1, 2} length:2], + [NSIndexPath indexPathWithIndexes:(NSUInteger[]){0, 3} length:2] + ]] + + ]; + + long n = 0; + for (NSArray *test in tests) { + NSIndexSet *insertions, *deletions; + NSArray *moves; + [test[0] asdk_diffWithArray:test[1] insertions:&insertions deletions:&deletions moves:&moves]; + NSMutableIndexSet *mutableInsertions = [insertions mutableCopy]; + NSMutableIndexSet *mutableDeletions = [deletions mutableCopy]; + + for (NSNumber *index in (NSArray *) test[2]) { + XCTAssert([mutableInsertions containsIndex:[index integerValue]], @"Test #%ld, insertions does not contain %ld", + n, (long)[index integerValue]); + [mutableInsertions removeIndex:(NSUInteger) [index integerValue]]; + } + for (NSNumber *index in (NSArray *) test[3]) { + XCTAssert([mutableDeletions containsIndex:[index integerValue]], @"Test #%ld, deletions does not contain %ld", + n, (long)[index integerValue]); + [mutableDeletions removeIndex:(NSUInteger) [index integerValue]]; + } + + XCTAssert([mutableInsertions count] == 0, @"Test #%ld, Unaccounted insertions: %@", n, mutableInsertions); + XCTAssert([mutableDeletions count] == 0, @"Test #%ld, Unaccounted deletions: %@", n, mutableDeletions); + + XCTAssert([moves isEqual:test[4]], @"Test #%ld, %@ !isEqual: %@", n, moves, test[4]); + n++; } } +- (void)testArrayDiffingRebuildingWithRandomElements +{ + NSArray *original = @[]; + NSArray *pending = @[]; + + NSIndexSet *insertions = nil; + NSIndexSet *deletions = nil; + NSArray *moves; + + for (int testNumber = 0; testNumber <= 25; testNumber++) { + int len = arc4random_uniform(10); + for (int j = 0; j < len; j++) { + original = [original arrayByAddingObject:@(arc4random_uniform(25))]; + } + len = arc4random_uniform(10); + for (int j = 0; j < len; j++) { + pending = [pending arrayByAddingObject:@(arc4random_uniform(25))]; + } + // Some sequences that presented issues in the past: + if (testNumber == 0) { + original = @[@20, @11, @14, @2, @14, @5, @4, @18, @0]; + pending = @[@9, @18, @18, @19, @20, @18, @22, @10, @3]; + } + if (testNumber == 1) { + original = @[@5, @9, @21, @11, @5, @9, @8]; + pending = @[@2, @12, @17, @19, @9, @1, @8, @5, @21]; + } + if (testNumber == 2) { + original = @[@14, @14, @12, @8, @20, @4, @0, @10]; + pending = @[@14]; + } + + [original asdk_diffWithArray:pending insertions:&insertions deletions:&deletions moves:&moves]; + + NSMutableArray *deletionsList = [NSMutableArray new]; + [deletions enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + [deletionsList addObject:@(idx)]; + }]; + NSMutableArray *insertionsList = [NSMutableArray new]; + [insertions enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + [insertionsList addObject:@(idx)]; + }]; + + NSUInteger i = 0; + NSUInteger j = 0; + NSMutableArray *test = [NSMutableArray new]; + for (NSUInteger count = 0; count < [pending count]; count++) { + if (i < [insertionsList count] && [insertionsList[i] unsignedIntegerValue] == count) { + [test addObject:pending[[insertionsList[i] unsignedIntegerValue]]]; + i++; + } else if (j < [moves count] && [moves[j] indexAtPosition:1] == count) { + [test addObject:original[[moves[j] indexAtPosition:0]]]; + j++; + } else { + [test addObject:original[count]]; + } + } + + XCTAssert([test isEqualToArray:pending], @"Did not mutate to expected new array:\n [%@] -> [%@], actual: [%@]\ninsertions: %@\nmoves: %@\ndeletions: %@", + [original componentsJoinedByString:@","], [pending componentsJoinedByString:@","], [test componentsJoinedByString:@","], + insertions, moves, deletions); + original = @[]; + pending = @[]; + } +} @end