mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-10 16:29:55 +00:00
Add move detection and support to ASLayoutTransition (#1006)
* Add move detection and support to ASLayoutTransition ...and NSArray+Diffing. Add some tests. * Update CHANGELOG.md * Update CHANGELOG.md * Update ASLayout+IGListKit.h * Update ASLayout+IGListKit.mm * Use std collections to avoid NSNumber boxing * Update ASLayoutTransition.mm * Code review updates. * Use `unordered_multimap` on stack instead of unordered_map<id,queue> on heap * Remove notFound BOOL (use NSNotFound sentinel value) and put some vars inside the if (insertions/moves) loop * Don't copy defaultCompare block (redundant under ARC) * Whitespace * Remove unneeded mutableCopy-s in ArrayDiffingTests * Code review updates. * Type _subnodeMoves pair.first to ASDisplayNode * instead of id * C++ enumeration * unowned refs for adding previousLayout nodes to _subnodeMoves * Remove unreleated ASDynamicCast that is probably right though * Add commentary to NSArray+Diffing.h; make multimap elements unowned * Use std::make_pair, optimize ASLayout+IGListKit * Oops I thought I had added these headers but nope * Simplify simplify * Diff subnodes instead of sublayouts * Another randomized test with actual ASLayouts
This commit is contained in:
parent
7c1aee7315
commit
8986838b48
@ -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 = "<group>"; };
|
||||
05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASSnapshotTestCase.m; sourceTree = "<group>"; };
|
||||
05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = "<group>"; };
|
||||
0FAFDF7320EC1C8F003A51C0 /* ASLayout+IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASLayout+IGListKit.h"; sourceTree = "<group>"; };
|
||||
0FAFDF7420EC1C90003A51C0 /* ASLayout+IGListKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayout+IGListKit.mm"; sourceTree = "<group>"; };
|
||||
18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = "<group>"; };
|
||||
18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionNode.mm; sourceTree = "<group>"; };
|
||||
1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = "<group>"; };
|
||||
@ -964,7 +968,7 @@
|
||||
DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = _ASTransitionContext.m; path = ../_ASTransitionContext.m; sourceTree = "<group>"; };
|
||||
DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASContextTransitioning.h; sourceTree = "<group>"; };
|
||||
DBC452D91C5BF64600B16017 /* NSArray+Diffing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Diffing.h"; sourceTree = "<group>"; };
|
||||
DBC452DA1C5BF64600B16017 /* NSArray+Diffing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Diffing.m"; sourceTree = "<group>"; };
|
||||
DBC452DA1C5BF64600B16017 /* NSArray+Diffing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSArray+Diffing.mm"; sourceTree = "<group>"; };
|
||||
DBC452DD1C5C6A6A00B16017 /* ArrayDiffingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ArrayDiffingTests.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
|
||||
DBC453211C5FD97200B16017 /* ASDisplayNodeImplicitHierarchyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASDisplayNodeImplicitHierarchyTests.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
|
||||
DBDB83921C6E879900D0098C /* ASPagerFlowLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPagerFlowLayout.h; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -685,7 +685,7 @@ ASLayoutElementStyleExtensibilityForwarding
|
||||
}
|
||||
|
||||
// Apply the subnode insertion immediately to be able to animate the nodes
|
||||
[pendingLayoutTransition applySubnodeInsertions];
|
||||
[pendingLayoutTransition applySubnodeInsertionsAndMoves];
|
||||
|
||||
// Kick off animating the layout transition
|
||||
{
|
||||
|
||||
@ -137,3 +137,4 @@
|
||||
|
||||
#import <AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.h>
|
||||
#import <AsyncDisplayKit/AsyncDisplayKit+IGListKitMethods.h>
|
||||
#import <AsyncDisplayKit/ASLayout+IGListKit.h>
|
||||
|
||||
@ -17,6 +17,60 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/**
|
||||
* 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<NSIndexPath *> **)moves;
|
||||
@end
|
||||
|
||||
@ -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 <AsyncDisplayKit/NSArray+Diffing.h>
|
||||
#import <AsyncDisplayKit/ASAssert.h>
|
||||
|
||||
@implementation NSArray (Diffing)
|
||||
|
||||
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
|
||||
{
|
||||
[self asdk_diffWithArray:array insertions:insertions deletions:deletions compareBlock:^BOOL(id lhs, id rhs) {
|
||||
return [lhs isEqual:rhs];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison
|
||||
{
|
||||
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
|
||||
185
Source/Details/NSArray+Diffing.mm
Normal file
185
Source/Details/NSArray+Diffing.mm
Normal file
@ -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 <AsyncDisplayKit/NSArray+Diffing.h>
|
||||
#import <UIKit/NSIndexPath+UIKitAdditions.h>
|
||||
#import <AsyncDisplayKit/ASAssert.h>
|
||||
#import <unordered_map>
|
||||
|
||||
@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<NSIndexPath *> **)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<NSIndexPath *> **)moves compareBlock:(compareBlock)comparison
|
||||
{
|
||||
struct NSObjectHash
|
||||
{
|
||||
std::size_t operator()(id <NSObject> k) const { return (std::size_t) [k hash]; };
|
||||
};
|
||||
struct NSObjectCompare
|
||||
{
|
||||
bool operator()(id <NSObject> lhs, id <NSObject> rhs) const { return (bool) [lhs isEqual:rhs]; };
|
||||
};
|
||||
std::unordered_multimap<unowned id, NSUInteger, NSObjectHash, NSObjectCompare> potentialMoves;
|
||||
|
||||
NSAssert(comparison != nil, @"Comparison block is required");
|
||||
NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)");
|
||||
NSMutableArray<NSIndexPath *> *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<id, NSUInteger>(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
|
||||
19
Source/Layout/ASLayout+IGListKit.h
Normal file
19
Source/Layout/ASLayout+IGListKit.h
Normal file
@ -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 <AsyncDisplayKit/ASLayout.h>
|
||||
#import <IGListKit/IGListKit.h>
|
||||
@interface ASLayout(IGListKit) <IGListDiffable>
|
||||
@end
|
||||
|
||||
#endif // AS_IG_LIST_KIT
|
||||
34
Source/Layout/ASLayout+IGListKit.mm
Normal file
34
Source/Layout/ASLayout+IGListKit.mm
Normal file
@ -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 <AsyncDisplayKit/ASAvailability.h>
|
||||
#if AS_IG_LIST_KIT
|
||||
#import "ASLayout+IGListKit.h"
|
||||
|
||||
@interface ASLayout() {
|
||||
@public
|
||||
id<ASLayoutElement> _layoutElement;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation ASLayout(IGListKit)
|
||||
|
||||
- (id <NSObject>)diffIdentifier
|
||||
{
|
||||
return self->_layoutElement;
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToDiffableObject:(id <IGListDiffable>)other
|
||||
{
|
||||
return [self isEqual:other];
|
||||
}
|
||||
@end
|
||||
#endif // AS_IG_LIST_KIT
|
||||
@ -283,6 +283,8 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
if (self == object) return YES;
|
||||
|
||||
ASLayout *layout = ASDynamicCast(object, ASLayout);
|
||||
if (layout == nil) {
|
||||
return NO;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -25,10 +25,11 @@
|
||||
#import <AsyncDisplayKit/ASLog.h>
|
||||
|
||||
#import <queue>
|
||||
#import <memory>
|
||||
|
||||
#import <AsyncDisplayKit/ASThread.h>
|
||||
#import <AsyncDisplayKit/ASEqualityHelpers.h>
|
||||
#if AS_IG_LIST_KIT
|
||||
#import <IGListKit/IGListKit.h>
|
||||
#import <AsyncDisplayKit/ASLayout+IGListKit.h>
|
||||
#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 @@ static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) {
|
||||
NSArray<ASDisplayNode *> *_insertedSubnodes;
|
||||
NSArray<ASDisplayNode *> *_removedSubnodes;
|
||||
std::vector<NSUInteger> _insertedSubnodePositions;
|
||||
std::vector<NSUInteger> _removedSubnodePositions;
|
||||
std::vector<std::pair<ASDisplayNode *, NSUInteger>> _subnodeMoves;
|
||||
}
|
||||
|
||||
- (instancetype)initWithNode:(ASDisplayNode *)node
|
||||
@ -98,27 +99,44 @@ static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) {
|
||||
|
||||
- (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 @@ static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) {
|
||||
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<id<ASLayoutElement>, NSUInteger> a,
|
||||
std::pair<ASDisplayNode *, NSUInteger> b) {
|
||||
return a.second < b.second;
|
||||
});
|
||||
#else
|
||||
NSIndexSet *insertions, *deletions;
|
||||
[previousLayout.sublayouts asdk_diffWithArray:pendingLayout.sublayouts
|
||||
NSArray<NSIndexPath *> *moves;
|
||||
NSArray<ASDisplayNode *> *previousNodes = [previousLayout.sublayouts valueForKey:@"layoutElement"];
|
||||
NSArray<ASDisplayNode *> *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 @@ static inline std::vector<NSUInteger> findNodesInLayoutAtIndexesWithFilteredNode
|
||||
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) {
|
||||
|
||||
@ -150,7 +150,11 @@
|
||||
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;
|
||||
|
||||
@ -2415,6 +2415,77 @@ static bool stringContainsPointer(NSString *description, id p) {
|
||||
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.
|
||||
@ -2425,14 +2496,26 @@ static bool stringContainsPointer(NSString *description, id p) {
|
||||
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;
|
||||
};
|
||||
|
||||
@ -2446,13 +2529,22 @@ static bool stringContainsPointer(NSString *description, id p) {
|
||||
|
||||
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
|
||||
|
||||
@ -127,7 +127,8 @@
|
||||
@[@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 @@
|
||||
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<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<NSIndexPath *> *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<NSNumber *> *original = @[];
|
||||
NSArray<NSNumber *> *pending = @[];
|
||||
|
||||
NSIndexSet *insertions = nil;
|
||||
NSIndexSet *deletions = nil;
|
||||
NSArray<NSIndexPath *> *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<NSNumber *> *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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user