Introduce ASIntegerMap, improve our changeset handling #trivial (#405)

* Introduce ASIntegerMap, improve our changeset handling

Rename to ASIntegerMap

License header

* Add unit tests for ASIntegerMap

* Address nit
This commit is contained in:
Adlai Holler
2017-07-05 13:29:02 -07:00
committed by GitHub
parent 03592e0669
commit d00ed249e5
10 changed files with 543 additions and 70 deletions

View File

@@ -327,7 +327,7 @@
CC0F886D1E4286FA00576FED /* ReferenceImages_iOS_10 in Resources */ = {isa = PBXBuildFile; fileRef = CC0F886A1E4286FA00576FED /* ReferenceImages_iOS_10 */; };
CC11F97A1DB181180024D77B /* ASNetworkImageNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */; };
CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */; };
CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */; };
CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */; };
CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20811C3F76D600798563 /* ASPendingStateController.h */; settings = {ATTRIBUTES = (Private, ); }; };
CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B20821C3F76D600798563 /* ASPendingStateController.mm */; };
CC3B208A1C3F7A5400798563 /* ASWeakSet.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20871C3F7A5400798563 /* ASWeakSet.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -343,6 +343,8 @@
CC55A70E1E529FA200594372 /* UIResponder+AsyncDisplayKit.m in Sources */ = {isa = PBXBuildFile; fileRef = CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.m */; };
CC55A7111E52A0F200594372 /* ASResponderChainEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */; };
CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */; };
CC56013B1F06E9A700DC4FBE /* ASIntegerMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */; };
CC56013C1F06E9A700DC4FBE /* ASIntegerMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */; };
CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; };
CC57EAF81E3939450034C595 /* ASTableView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; };
CC583AD61EF9BDBE00134156 /* ASTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = CC583AC21EF9BAB400134156 /* ASTestCase.m */; };
@@ -399,6 +401,7 @@
CCCCCCE71EC3F0FC0087FE10 /* NSAttributedString+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCCCCE51EC3F0FC0087FE10 /* NSAttributedString+ASText.h */; };
CCCCCCE81EC3F0FC0087FE10 /* NSAttributedString+ASText.m in Sources */ = {isa = PBXBuildFile; fileRef = CCCCCCE61EC3F0FC0087FE10 /* NSAttributedString+ASText.m */; };
CCDD148B1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCDD148A1EEDCD9D0020834E /* ASCollectionModernDataSourceTests.m */; };
CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */; };
CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; };
DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; };
DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; };
@@ -788,7 +791,7 @@
CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASNetworkImageNodeTests.m; sourceTree = "<group>"; };
CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionView+Undeprecated.h"; sourceTree = "<group>"; };
CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableElementMap.h; sourceTree = "<group>"; };
CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMutableElementMap.mm; sourceTree = "<group>"; };
CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMutableElementMap.m; sourceTree = "<group>"; };
CC3B20811C3F76D600798563 /* ASPendingStateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPendingStateController.h; sourceTree = "<group>"; };
CC3B20821C3F76D600798563 /* ASPendingStateController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPendingStateController.mm; sourceTree = "<group>"; };
CC3B20871C3F7A5400798563 /* ASWeakSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakSet.h; sourceTree = "<group>"; };
@@ -807,6 +810,8 @@
CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+AsyncDisplayKit.m"; sourceTree = "<group>"; };
CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASResponderChainEnumerator.h; sourceTree = "<group>"; };
CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASResponderChainEnumerator.m; sourceTree = "<group>"; };
CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIntegerMap.h; sourceTree = "<group>"; };
CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASIntegerMap.mm; sourceTree = "<group>"; };
CC57EAF91E394EA40034C595 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "ASDisplayNode+OCMock.m"; sourceTree = "<group>"; };
CC583AC11EF9BAB400134156 /* ASTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTestCase.h; sourceTree = "<group>"; };
@@ -873,6 +878,7 @@
CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = "<group>"; };
CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = "<group>"; };
CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = "<group>"; };
CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIntegerMapTests.m; sourceTree = "<group>"; };
D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = "<group>"; };
D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = "<group>"; };
D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = "<group>"; };
@@ -1132,6 +1138,7 @@
CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */,
CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */,
CC051F1E1D7A286A006434CB /* ASCALayerTests.m */,
CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */,
CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */,
CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */,
CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */,
@@ -1210,6 +1217,8 @@
058D09E1195D050800B7D73C /* Details */ = {
isa = PBXGroup;
children = (
CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */,
CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */,
CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */,
CC0F885D1E4280B800576FED /* _ASCollectionViewCell.m */,
3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */,
@@ -1318,7 +1327,7 @@
CCA282B21E9EA7310037E8B7 /* ASTipsController.h */,
CCA282B31E9EA7310037E8B7 /* ASTipsController.m */,
CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */,
CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */,
CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */,
E5ABAC791E8564EE007AC15C /* ASRectTable.h */,
E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */,
CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */,
@@ -1713,6 +1722,7 @@
509E68631B3AEDB4009B9150 /* ASCollectionViewLayoutController.h in Headers */,
B35061F71B010EFD0018CF92 /* ASCollectionViewProtocols.h in Headers */,
68FC85E31CE29B7E00EDD713 /* ASTabBarController.h in Headers */,
CC56013B1F06E9A700DC4FBE /* ASIntegerMap.h in Headers */,
B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */,
E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */,
B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */,
@@ -2089,6 +2099,7 @@
69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.m in Sources */,
CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */,
CC54A81E1D7008B300296A24 /* ASDispatchTests.m in Sources */,
CCE4F9B31F0D60AC00062E4E /* ASIntegerMapTests.m in Sources */,
058D0A3B195D057000B7D73C /* ASDisplayNodeTestsHelper.m in Sources */,
83A7D95E1D446A6E00BF333E /* ASWeakMapTests.m in Sources */,
056D21551ABCEF50001107EF /* ASImageNodeSnapshotTests.m in Sources */,
@@ -2210,7 +2221,7 @@
B350621E1B010EFD0018CF92 /* ASHighlightOverlayLayer.mm in Sources */,
9CC606651D24DF9E006581A0 /* NSIndexSet+ASHelpers.m in Sources */,
CC0F885F1E4280B800576FED /* _ASCollectionViewCell.m in Sources */,
CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.mm in Sources */,
CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.m in Sources */,
B35062541B010EFD0018CF92 /* ASImageNode+CGExtras.m in Sources */,
E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */,
6947B0C01E36B4E30007C478 /* ASStackUnpositionedLayout.mm in Sources */,
@@ -2279,6 +2290,7 @@
254C6B831BF94F8A003EC431 /* ASTextKitCoreTextAdditions.m in Sources */,
CCCCCCE21EC3EF060087FE10 /* ASTextUtilities.m in Sources */,
CC55A70E1E529FA200594372 /* UIResponder+AsyncDisplayKit.m in Sources */,
CC56013C1F06E9A700DC4FBE /* ASIntegerMap.mm in Sources */,
697796611D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.mm in Sources */,
B350623B1B010EFD0018CF92 /* NSMutableAttributedString+TextKitAdditions.m in Sources */,
CCA282CD1E9EB73E0037E8B7 /* ASTipNode.m in Sources */,

View File

@@ -703,7 +703,7 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray<ASCollectionElement *> *
}
// Migrate old supplementary nodes to their new index paths.
[map migrateSupplementaryElementsWithChangeSet:changeSet];
[map migrateSupplementaryElementsWithSectionMapping:changeSet.sectionMapping];
for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) {
[map removeItemsAtIndexPaths:change.indexPaths];

View File

@@ -0,0 +1,70 @@
//
// ASIntegerMap.h
// Texture
//
// Copyright (c) 2017-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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
NS_ASSUME_NONNULL_BEGIN
/**
* An objective-C wrapper for unordered_map.
*/
AS_SUBCLASSING_RESTRICTED
@interface ASIntegerMap : NSObject <NSCopying>
/**
* Creates a map based on the specified update to an array.
*
* If oldCount is 0, returns the empty map.
* If deleted and inserted are empty, returns the identity map.
*/
+ (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount
deleted:(nullable NSIndexSet *)deleted
inserted:(nullable NSIndexSet *)inserted;
/**
* A singleton that maps each integer to itself. Its inverse is itself.
*
* Note: You cannot mutate this.
*/
@property (class, atomic, readonly) ASIntegerMap *identityMap;
/**
* A singleton that returns NSNotFound for all keys. Its inverse is itself.
*
* Note: You cannot mutate this.
*/
@property (class, atomic, readonly) ASIntegerMap *emptyMap;
/**
* Retrieves the integer for a given key, or NSNotFound if the key is not found.
*
* @param key A key to lookup the value for.
*/
- (NSInteger)integerForKey:(NSInteger)key;
/**
* Sets the value for a given key.
*
* @param value The new value.
* @param key The key to store the value for.
*/
- (void)setInteger:(NSInteger)value forKey:(NSInteger)key;
/**
* Create and return a map with the inverse mapping.
*/
- (ASIntegerMap *)inverseMap;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,188 @@
//
// ASIntegerMap.mm
// Texture
//
// Copyright (c) 2017-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 "ASIntegerMap.h"
#import <AsyncDisplayKit/ASAssert.h>
#import <unordered_map>
#import <NSIndexSet+ASHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
/**
* This is just a friendly Objective-C interface to unordered_map<NSInteger, NSInteger>
*/
@interface ASIntegerMap () <ASDescriptionProvider>
@end
@implementation ASIntegerMap {
std::unordered_map<NSInteger, NSInteger> _map;
BOOL _isIdentity;
BOOL _isEmpty;
BOOL _immutable; // identity map and empty mape are immutable.
}
#pragma mark - Singleton
+ (ASIntegerMap *)identityMap
{
static ASIntegerMap *identityMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
identityMap = [[ASIntegerMap alloc] init];
identityMap->_isIdentity = YES;
identityMap->_immutable = YES;
});
return identityMap;
}
+ (ASIntegerMap *)emptyMap
{
static ASIntegerMap *emptyMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
emptyMap = [[ASIntegerMap alloc] init];
emptyMap->_isEmpty = YES;
emptyMap->_immutable = YES;
});
return emptyMap;
}
+ (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount deleted:(NSIndexSet *)deletions inserted:(NSIndexSet *)insertions
{
if (oldCount == 0) {
return ASIntegerMap.emptyMap;
}
if (deletions.count == 0 && insertions.count == 0) {
return ASIntegerMap.identityMap;
}
ASIntegerMap *result = [[ASIntegerMap alloc] init];
// Start with the old indexes
NSMutableIndexSet *indexes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)];
// Descending order, shift deleted ranges left
[deletions enumerateRangesWithOptions:NSEnumerationReverse usingBlock:^(NSRange range, BOOL * _Nonnull stop) {
[indexes shiftIndexesStartingAtIndex:NSMaxRange(range) by:-range.length];
}];
// Ascending order, shift inserted ranges right
[insertions enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
[indexes shiftIndexesStartingAtIndex:range.location by:range.length];
}];
__block NSInteger oldIndex = 0;
[indexes enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) {
// Note we advance oldIndex unconditionally, not newIndex
for (NSInteger newIndex = range.location; newIndex < NSMaxRange(range); oldIndex++) {
if ([deletions containsIndex:oldIndex]) {
// index was deleted, do nothing, just let oldIndex advance.
} else {
// assign the next index for this item.
result->_map[oldIndex] = newIndex++;
}
}
}];
return result;
}
- (NSInteger)integerForKey:(NSInteger)key
{
if (_isIdentity) {
return key;
} else if (_isEmpty) {
return NSNotFound;
}
auto result = _map.find(key);
return result != _map.end() ? result->second : NSNotFound;
}
- (void)setInteger:(NSInteger)value forKey:(NSInteger)key
{
if (_immutable) {
ASDisplayNodeFailAssert(@"Cannot mutate special integer map: %@", self);
return;
}
_map[key] = value;
}
- (ASIntegerMap *)inverseMap
{
if (_isIdentity || _isEmpty) {
return self;
}
auto result = [[ASIntegerMap alloc] init];
for (auto it = _map.begin(); it != _map.end(); it++) {
result->_map[it->second] = it->first;
}
return result;
}
#pragma mark - NSCopying
- (id)copyWithZone:(NSZone *)zone
{
if (_immutable) {
return self;
}
auto newMap = [[ASIntegerMap allocWithZone:zone] init];
newMap->_map = _map;
return newMap;
}
#pragma mark - Description
- (NSMutableArray<NSDictionary *> *)propertiesForDescription
{
NSMutableArray *result = [NSMutableArray array];
if (_isIdentity) {
[result addObject:@{ @"map": @"<identity>" }];
} else if (_isEmpty) {
[result addObject:@{ @"map": @"<empty>" }];
} else {
// { 1->2 3->4 5->6 }
NSMutableString *str = [NSMutableString string];
for (auto it = _map.begin(); it != _map.end(); it++) {
[str appendFormat:@" %zd->%zd", it->first, it->second];
}
// Remove leading space
if (str.length > 0) {
[str deleteCharactersInRange:NSMakeRange(0, 1)];
}
[result addObject:@{ @"map": str }];
}
return result;
}
- (NSString *)description
{
return ASObjectDescriptionMakeWithoutObject([self propertiesForDescription]);
}
- (BOOL)isEqual:(id)object
{
if ([super isEqual:object]) {
return YES;
}
if (auto otherMap = ASDynamicCast(object, ASIntegerMap)) {
return otherMap->_map == _map;
}
return NO;
}
@end

View File

@@ -18,6 +18,7 @@
#import <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASIntegerMap.h>
NS_ASSUME_NONNULL_BEGIN
@@ -57,7 +58,7 @@ AS_SUBCLASSING_RESTRICTED
*
* This also deletes any supplementary elements in deleted sections.
*/
- (void)migrateSupplementaryElementsWithChangeSet:(_ASHierarchyChangeSet *)changeSet;
- (void)migrateSupplementaryElementsWithSectionMapping:(ASIntegerMap *)mapping;
@end

View File

@@ -1,5 +1,5 @@
//
// ASMutableElementMap.mm
// ASMutableElementMap.m
// Texture
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
@@ -22,7 +22,6 @@
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASTwoDimensionalArrayUtils.h>
#import <AsyncDisplayKit/NSIndexSet+ASHelpers.h>
#import <AsyncDisplayKit/_ASHierarchyChangeSet.h>
typedef NSMutableArray<NSMutableArray<ASCollectionElement *> *> ASMutableCollectionElementTwoDimensionalArray;
@@ -102,9 +101,10 @@ typedef NSMutableDictionary<NSString *, NSMutableDictionary<NSIndexPath *, ASCol
}
}
- (void)migrateSupplementaryElementsWithChangeSet:(_ASHierarchyChangeSet *)changeSet
- (void)migrateSupplementaryElementsWithSectionMapping:(ASIntegerMap *)mapping
{
if (changeSet.deletedSections.count == 0 && changeSet.insertedSections.count == 0) {
// Fast-path, no section changes.
if (mapping == ASIntegerMap.identityMap) {
return;
}
@@ -118,7 +118,7 @@ typedef NSMutableDictionary<NSString *, NSMutableDictionary<NSIndexPath *, ASCol
NSMutableDictionary *newSupps = [NSMutableDictionary dictionary];
[supps enumerateKeysAndObjectsUsingBlock:^(NSIndexPath * _Nonnull oldIndexPath, ASCollectionElement * _Nonnull obj, BOOL * _Nonnull stop) {
NSInteger oldSection = oldIndexPath.section;
NSInteger newSection = [changeSet newSectionForOldSection:oldSection];
NSInteger newSection = [mapping integerForKey:oldSection];
if (oldSection == newSection) {
// Index path stayed the same, just copy it over.

View File

@@ -18,6 +18,7 @@
#import <Foundation/Foundation.h>
#import <vector>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASIntegerMap.h>
#import <AsyncDisplayKit/ASLog.h>
NS_ASSUME_NONNULL_BEGIN
@@ -91,7 +92,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType);
@property (nonatomic, readonly) _ASHierarchyChangeType changeType;
+ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes;
+ (NSDictionary<NSNumber *, NSIndexSet *> *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes;
/**
* If this is a .OriginalInsert or .OriginalDelete change, this returns a copied change
@@ -154,6 +155,28 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType);
*/
- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection;
/**
* A table that maps old section indexes to new section indexes.
*/
@property (nonatomic, readonly, strong) ASIntegerMap *sectionMapping;
/**
* A table that maps new section indexes to old section indexes.
*/
@property (nonatomic, readonly, strong) ASIntegerMap *reverseSectionMapping;
/**
* A table that provides the item mapping for the old section. If the section was deleted
* or is out of bounds, returns the empty table.
*/
- (ASIntegerMap *)itemMappingInSection:(NSInteger)oldSection;
/**
* A table that provides the reverse item mapping for the new section. If the section was inserted
* or is out of bounds, returns the empty table.
*/
- (ASIntegerMap *)reverseItemMappingInSection:(NSInteger)newSection;
/**
* Get the old item index path for the given new index path.
*
@@ -162,6 +185,14 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType);
*/
- (nullable NSIndexPath *)oldIndexPathForNewIndexPath:(NSIndexPath *)indexPath;
/**
* Get the new item index path for the given old index path.
*
* @precondition The change set must be completed.
* @return The new index path, or nil if the given item was deleted.
*/
- (nullable NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath;
/// Call this once the change set has been constructed to prevent future modifications to the changeset. Calling this more than once is a programmer error.
/// NOTE: Calling this method will cause the changeset to convert all reloads into delete/insert pairs.
- (void)markCompletedWithNewItemCounts:(std::vector<NSInteger>)newItemCounts;

View File

@@ -101,6 +101,12 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType)
@interface _ASHierarchyChangeSet ()
// array index is old section index, map goes oldItem -> newItem
@property (nonatomic, strong, readonly) NSMutableArray<ASIntegerMap *> *itemMappings;
// array index is new section index, map goes newItem -> oldItem
@property (nonatomic, strong, readonly) NSMutableArray<ASIntegerMap *> *reverseItemMappings;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges;
@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *originalInsertItemChanges;
@@ -124,6 +130,10 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType)
std::vector<NSInteger> _newItemCounts;
void (^_completionHandler)(BOOL finished);
}
@synthesize sectionMapping = _sectionMapping;
@synthesize reverseSectionMapping = _reverseSectionMapping;
@synthesize itemMappings = _itemMappings;
@synthesize reverseItemMappings = _reverseItemMappings;
- (instancetype)init
{
@@ -244,55 +254,121 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType)
- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection
{
ASDisplayNodeAssertNotNil(_deletedSections, @"Cannot call %@ before `markCompleted` returns.", NSStringFromSelector(_cmd));
ASDisplayNodeAssertNotNil(_insertedSections, @"Cannot call %@ before `markCompleted` returns.", NSStringFromSelector(_cmd));
[self _ensureCompleted];
if ([_deletedSections containsIndex:oldSection]) {
return NSNotFound;
}
NSUInteger newIndex = oldSection - [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldSection)];
newIndex += [_insertedSections as_indexChangeByInsertingItemsBelowIndex:newIndex];
return newIndex;
return [self.sectionMapping integerForKey:oldSection];
}
- (NSUInteger)oldSectionForNewSection:(NSUInteger)newSection
{
[self _ensureCompleted];
if ([_insertedSections containsIndex:newSection]) {
return NSNotFound;
return [self.reverseSectionMapping integerForKey:newSection];
}
NSInteger oldIndex = newSection - [_insertedSections as_indexChangeByInsertingItemsBelowIndex:newSection];
oldIndex += [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldIndex)];
return oldIndex;
- (ASIntegerMap *)sectionMapping
{
ASDisplayNodeAssertNotNil(_deletedSections, @"Cannot call %s before `markCompleted` returns.", sel_getName(_cmd));
ASDisplayNodeAssertNotNil(_insertedSections, @"Cannot call %s before `markCompleted` returns.", sel_getName(_cmd));
[self _ensureCompleted];
if (_sectionMapping == nil) {
_sectionMapping = [ASIntegerMap mapForUpdateWithOldCount:_oldItemCounts.size() deleted:_deletedSections inserted:_insertedSections];
}
return _sectionMapping;
}
- (ASIntegerMap *)reverseSectionMapping
{
if (_reverseSectionMapping == nil) {
_reverseSectionMapping = [self.sectionMapping inverseMap];
}
return _reverseSectionMapping;
}
- (NSMutableArray *)itemMappings
{
[self _ensureCompleted];
if (_itemMappings == nil) {
_itemMappings = [NSMutableArray array];
auto insertMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_originalInsertItemChanges];
auto deleteMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_originalDeleteItemChanges];
NSInteger oldSection = 0;
for (auto oldCount : _oldItemCounts) {
NSInteger newSection = [self newSectionForOldSection:oldSection];
ASIntegerMap *table;
if (newSection == NSNotFound) {
table = ASIntegerMap.emptyMap;
} else {
table = [ASIntegerMap mapForUpdateWithOldCount:oldCount deleted:deleteMap[@(oldSection)] inserted:insertMap[@(newSection)]];
}
_itemMappings[oldSection] = table;
oldSection++;
}
}
return _itemMappings;
}
- (NSMutableArray *)reverseItemMappings
{
[self _ensureCompleted];
if (_reverseItemMappings == nil) {
_reverseItemMappings = [NSMutableArray array];
for (NSInteger newSection = 0; newSection < _newItemCounts.size(); newSection++) {
NSInteger oldSection = [self oldSectionForNewSection:newSection];
ASIntegerMap *table;
if (oldSection == NSNotFound) {
table = ASIntegerMap.emptyMap;
} else {
table = [[self itemMappingInSection:oldSection] inverseMap];
}
_reverseItemMappings[newSection] = table;
}
}
return _reverseItemMappings;
}
- (ASIntegerMap *)itemMappingInSection:(NSInteger)oldSection
{
if (self.includesReloadData || oldSection >= _oldItemCounts.size()) {
return ASIntegerMap.emptyMap;
}
return self.itemMappings[oldSection];
}
- (ASIntegerMap *)reverseItemMappingInSection:(NSInteger)newSection
{
if (self.includesReloadData || newSection >= _newItemCounts.size()) {
return ASIntegerMap.emptyMap;
}
return self.reverseItemMappings[newSection];
}
- (NSIndexPath *)oldIndexPathForNewIndexPath:(NSIndexPath *)indexPath
{
[self _ensureCompleted];
// Inserted sections return nil.
NSInteger newSection = indexPath.section;
NSInteger newItem = indexPath.item;
NSInteger oldSection = [self oldSectionForNewSection:newSection];
if (oldSection == NSNotFound) {
return nil;
}
// Inserted items return nil.
for (_ASHierarchyItemChange *change in _originalInsertItemChanges) {
if ([change.indexPaths containsObject:indexPath]) {
NSInteger oldItem = [[self reverseItemMappingInSection:newSection] integerForKey:indexPath.item];
if (oldItem == NSNotFound) {
return nil;
}
return [NSIndexPath indexPathForItem:oldItem inSection:oldSection];
}
// TODO: This is a pretty inefficient way to do this.
NSIndexSet *insertsInSection = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_insertItemChanges][@(newSection)];
NSIndexSet *deletesInSection = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_deleteItemChanges][@(oldSection)];
NSInteger oldIndex = newItem - [insertsInSection as_indexChangeByInsertingItemsBelowIndex:newItem];
oldIndex += [deletesInSection countOfIndexesInRange:NSMakeRange(0, oldIndex)];
return [NSIndexPath indexPathForItem:oldIndex inSection:oldSection];
- (NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath
{
[self _ensureCompleted];
NSInteger oldSection = indexPath.section;
NSInteger newSection = [self newSectionForOldSection:oldSection];
if (newSection == NSNotFound) {
return nil;
}
NSInteger newItem = [[self itemMappingInSection:oldSection] integerForKey:indexPath.item];
if (newItem == NSNotFound) {
return nil;
}
return [NSIndexPath indexPathForItem:newItem inSection:newSection];
}
- (void)reloadData
@@ -424,34 +500,12 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType)
}
[_ASHierarchyItemChange ensureItemChanges:_insertItemChanges ofSameType:_ASHierarchyChangeTypeInsert];
NSDictionary *insertedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_insertItemChanges];
[_ASHierarchyItemChange ensureItemChanges:_deleteItemChanges ofSameType:_ASHierarchyChangeTypeDelete];
NSDictionary *deletedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_deleteItemChanges];
for (_ASHierarchyItemChange *change in _reloadItemChanges) {
NSAssert(change.changeType == _ASHierarchyChangeTypeReload, @"It must be a reload change to be in here");
NSMutableArray *newIndexPaths = [NSMutableArray arrayWithCapacity:change.indexPaths.count];
// Every indexPaths in the change need to update its section and/or row
// depending on all the deletions and insertions
// For reference, when batching reloads/deletes/inserts:
// - delete/reload indexPaths that are passed in should all be their current indexPaths
// - insert indexPaths that are passed in should all be their future indexPaths after deletions
for (NSIndexPath *indexPath in change.indexPaths) {
NSUInteger section = [self newSectionForOldSection:indexPath.section];
NSUInteger item = indexPath.item;
// Update row number based on deletions that are above the current row in the current section
NSIndexSet *indicesDeletedInSection = deletedIndexPathsMap[@(indexPath.section)];
item -= [indicesDeletedInSection countOfIndexesInRange:NSMakeRange(0, item)];
// Update row number based on insertions that are above the current row in the future section
NSIndexSet *indicesInsertedInSection = insertedIndexPathsMap[@(section)];
item += [indicesInsertedInSection as_indexChangeByInsertingItemsBelowIndex:item];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:item inSection:section];
[newIndexPaths addObject:newIndexPath];
}
auto newIndexPaths = ASArrayByFlatMapping(change.indexPaths, NSIndexPath *indexPath, [self newIndexPathForOldIndexPath:indexPath]);
// All reload changes are translated into deletes and inserts
// We delete the items that needs reload together with other deleted items, at their original index

117
Tests/ASIntegerMapTests.m Normal file
View File

@@ -0,0 +1,117 @@
//
// ASIntegerMapTests.m
// Texture
//
// Copyright (c) 2017-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 "ASTestCase.h"
#import "ASIntegerMap.h"
@interface ASIntegerMapTests : ASTestCase
@end
@implementation ASIntegerMapTests
- (void)testIsEqual
{
ASIntegerMap *map = [[ASIntegerMap alloc] init];
[map setInteger:1 forKey:0];
ASIntegerMap *alsoMap = [[ASIntegerMap alloc] init];
[alsoMap setInteger:1 forKey:0];
ASIntegerMap *notMap = [[ASIntegerMap alloc] init];
[notMap setInteger:2 forKey:0];
XCTAssertEqualObjects(map, alsoMap);
XCTAssertNotEqualObjects(map, notMap);
}
#pragma mark - Changeset mapping
/// 1 item, no changes -> identity map
- (void)testEmptyChange
{
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:1 deleted:nil inserted:nil];
XCTAssertEqual(map, ASIntegerMap.identityMap);
}
/// 0 items -> empty map
- (void)testChangeOnNoData
{
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:0 deleted:nil inserted:nil];
XCTAssertEqual(map, ASIntegerMap.emptyMap);
}
/// 2 items, delete 0
- (void)testBasicChange1
{
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:2 deleted:[NSIndexSet indexSetWithIndex:0] inserted:nil];
XCTAssertEqual([map integerForKey:0], NSNotFound);
XCTAssertEqual([map integerForKey:1], 0);
XCTAssertEqual([map integerForKey:2], NSNotFound);
}
/// 2 items, insert 0
- (void)testBasicChange2
{
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:2 deleted:nil inserted:[NSIndexSet indexSetWithIndex:0]];
XCTAssertEqual([map integerForKey:0], 1);
XCTAssertEqual([map integerForKey:1], 2);
XCTAssertEqual([map integerForKey:2], NSNotFound);
}
/// 2 items, insert 0, delete 0
- (void)testChange1
{
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:2 deleted:[NSIndexSet indexSetWithIndex:0] inserted:[NSIndexSet indexSetWithIndex:0]];
XCTAssertEqual([map integerForKey:0], NSNotFound);
XCTAssertEqual([map integerForKey:1], 1);
XCTAssertEqual([map integerForKey:2], NSNotFound);
}
/// 4 items, insert {0-1, 3}
- (void)testChange2
{
NSMutableIndexSet *inserts = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)];
[inserts addIndex:3];
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:4 deleted:nil inserted:inserts];
XCTAssertEqual([map integerForKey:0], 2);
XCTAssertEqual([map integerForKey:1], 4);
XCTAssertEqual([map integerForKey:2], 5);
XCTAssertEqual([map integerForKey:3], 6);
}
/// 4 items, delete {0-1, 3}
- (void)testChange3
{
NSMutableIndexSet *deletes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)];
[deletes addIndex:3];
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:4 deleted:deletes inserted:nil];
XCTAssertEqual([map integerForKey:0], NSNotFound);
XCTAssertEqual([map integerForKey:1], NSNotFound);
XCTAssertEqual([map integerForKey:2], 0);
XCTAssertEqual([map integerForKey:3], NSNotFound);
}
/// 5 items, delete {0-1, 3} insert {1-2, 4}
- (void)testChange4
{
NSMutableIndexSet *deletes = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)];
[deletes addIndex:3];
NSMutableIndexSet *inserts = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(1, 2)];
[inserts addIndex:4];
ASIntegerMap *map = [ASIntegerMap mapForUpdateWithOldCount:5 deleted:deletes inserted:inserts];
XCTAssertEqual([map integerForKey:0], NSNotFound);
XCTAssertEqual([map integerForKey:1], NSNotFound);
XCTAssertEqual([map integerForKey:2], 0);
XCTAssertEqual([map integerForKey:3], NSNotFound);
XCTAssertEqual([map integerForKey:4], 3);
XCTAssertEqual([map integerForKey:5], NSNotFound);
}
@end

View File

@@ -75,7 +75,7 @@ static __weak ASTestCase *currentTestCase;
// Go ahead and spin the run loop before finishing, so the system
// unregisters/cleans up whatever possible.
[NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
[NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantPast];
[super tearDown];
}