From 2183b38321c254494d8b617f26b76d7aa975a889 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 30 Jun 2017 16:59:39 -0700 Subject: [PATCH] Introduce ASIntegerMap, improve our changeset handling Rename to ASIntegerMap License header --- AsyncDisplayKit.xcodeproj/project.pbxproj | 16 +- Source/Details/ASDataController.mm | 2 +- Source/Details/ASIntegerMap.h | 58 +++++++ Source/Details/ASIntegerMap.mm | 156 +++++++++++++++++ Source/Private/ASMutableElementMap.h | 3 +- ...bleElementMap.mm => ASMutableElementMap.m} | 10 +- Source/Private/_ASHierarchyChangeSet.h | 33 +++- Source/Private/_ASHierarchyChangeSet.mm | 164 ++++++++++++------ 8 files changed, 375 insertions(+), 67 deletions(-) create mode 100644 Source/Details/ASIntegerMap.h create mode 100644 Source/Details/ASIntegerMap.mm rename Source/Private/{ASMutableElementMap.mm => ASMutableElementMap.m} (94%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 4c4cbe3af..93425d57a 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -325,7 +325,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, ); }; }; @@ -341,6 +341,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 */; }; @@ -784,7 +786,7 @@ CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASNetworkImageNodeTests.m; sourceTree = ""; }; CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionView+Undeprecated.h"; sourceTree = ""; }; CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableElementMap.h; sourceTree = ""; }; - CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMutableElementMap.mm; sourceTree = ""; }; + CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMutableElementMap.m; sourceTree = ""; }; CC3B20811C3F76D600798563 /* ASPendingStateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPendingStateController.h; sourceTree = ""; }; CC3B20821C3F76D600798563 /* ASPendingStateController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPendingStateController.mm; sourceTree = ""; }; CC3B20871C3F7A5400798563 /* ASWeakSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakSet.h; sourceTree = ""; }; @@ -803,6 +805,8 @@ CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIResponder+AsyncDisplayKit.m"; sourceTree = ""; }; CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASResponderChainEnumerator.h; sourceTree = ""; }; CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASResponderChainEnumerator.m; sourceTree = ""; }; + CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASIntegerMap.h; sourceTree = ""; }; + CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASIntegerMap.mm; sourceTree = ""; }; CC57EAF91E394EA40034C595 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CC583AC01EF9BAB400134156 /* ASDisplayNode+OCMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "ASDisplayNode+OCMock.m"; sourceTree = ""; }; CC583AC11EF9BAB400134156 /* ASTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTestCase.h; sourceTree = ""; }; @@ -1204,6 +1208,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */, + CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */, CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */, CC0F885D1E4280B800576FED /* _ASCollectionViewCell.m */, 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */, @@ -1314,7 +1320,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 */, @@ -1707,6 +1713,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 */, @@ -2203,7 +2210,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 */, @@ -2271,6 +2278,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 */, diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index bace28112..d74795494 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -675,7 +675,7 @@ - (void)_updateElementsInMap:(ASMutableElementMap *)map } // 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]; diff --git a/Source/Details/ASIntegerMap.h b/Source/Details/ASIntegerMap.h new file mode 100644 index 000000000..dc82083dc --- /dev/null +++ b/Source/Details/ASIntegerMap.h @@ -0,0 +1,58 @@ +// +// 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An objective-C wrapper for unordered_map. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASIntegerMap : NSObject + +/** + * Creates an 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:(NSIndexSet *)deleted + inserted:(NSIndexSet *)inserted; + +/** + * A singleton that maps each integer to itself. Its inverse is itself. + */ +@property (class, atomic, readonly) ASIntegerMap *identityMap; + +/** + * A singleton that returns NSNotFound for all keys. Its inverse is itself. + */ +@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; + +/** + * Create and return a map with the inverse mapping. + */ +- (ASIntegerMap *)inverseMap; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASIntegerMap.mm b/Source/Details/ASIntegerMap.mm new file mode 100644 index 000000000..fea08eb5b --- /dev/null +++ b/Source/Details/ASIntegerMap.mm @@ -0,0 +1,156 @@ +// +// 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 +#import +#import + +/** + * This is just a friendly Objective-C interface to unordered_map + */ +@interface ASIntegerMap () +@end + +@implementation ASIntegerMap { + std::unordered_map _map; + BOOL _isIdentity; + BOOL _isEmpty; +} + +#pragma mark - Singleton + ++ (ASIntegerMap *)identityMap +{ + static ASIntegerMap *identityMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + identityMap = [[ASIntegerMap alloc] init]; + identityMap->_isIdentity = YES; + }); + return identityMap; +} + ++ (ASIntegerMap *)emptyMap +{ + static ASIntegerMap *emptyMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + emptyMap = [[ASIntegerMap alloc] init]; + emptyMap->_isEmpty = 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; +} + +- (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 +{ + return self; +} + +#pragma mark - Description + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if (_isIdentity) { + [result addObject:@{ @"map": @"" }]; + } else if (_isEmpty) { + [result addObject:@{ @"map": @"" }]; + } 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]); +} + +@end diff --git a/Source/Private/ASMutableElementMap.h b/Source/Private/ASMutableElementMap.h index b5636cd66..3b5c9bd7f 100644 --- a/Source/Private/ASMutableElementMap.h +++ b/Source/Private/ASMutableElementMap.h @@ -18,6 +18,7 @@ #import #import #import +#import 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 diff --git a/Source/Private/ASMutableElementMap.mm b/Source/Private/ASMutableElementMap.m similarity index 94% rename from Source/Private/ASMutableElementMap.mm rename to Source/Private/ASMutableElementMap.m index 055c03fcc..7ce12dca8 100644 --- a/Source/Private/ASMutableElementMap.mm +++ b/Source/Private/ASMutableElementMap.m @@ -1,5 +1,5 @@ // -// ASMutableElementMap.mm +// ASMutableElementMap.m // Texture // // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. @@ -22,7 +22,6 @@ #import #import #import -#import typedef NSMutableArray *> ASMutableCollectionElementTwoDimensionalArray; @@ -102,9 +101,10 @@ - (void)insertElement:(ASCollectionElement *)element atIndexPath:(NSIndexPath *) } } -- (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 @@ - (void)migrateSupplementaryElementsWithChangeSet:(_ASHierarchyChangeSet *)chang 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. diff --git a/Source/Private/_ASHierarchyChangeSet.h b/Source/Private/_ASHierarchyChangeSet.h index a5046b4b9..38d83fbea 100644 --- a/Source/Private/_ASHierarchyChangeSet.h +++ b/Source/Private/_ASHierarchyChangeSet.h @@ -18,6 +18,7 @@ #import #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -90,7 +91,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); @property (nonatomic, readonly) _ASHierarchyChangeType changeType; -+ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes; ++ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes; /** * If this is a .OriginalInsert or .OriginalDelete change, this returns a copied change @@ -147,6 +148,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. * @@ -155,6 +178,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)newItemCounts; diff --git a/Source/Private/_ASHierarchyChangeSet.mm b/Source/Private/_ASHierarchyChangeSet.mm index 967ecc99d..5a38e5c8c 100644 --- a/Source/Private/_ASHierarchyChangeSet.mm +++ b/Source/Private/_ASHierarchyChangeSet.mm @@ -99,7 +99,13 @@ + (NSString *)smallDescriptionForItemChanges:(NSArray<_ASHierarchyItemChange *> + (void)ensureItemChanges:(NSArray<_ASHierarchyItemChange *> *)changes ofSameType:(_ASHierarchyChangeType)changeType; @end -@interface _ASHierarchyChangeSet () +@interface _ASHierarchyChangeSet () + +// array index is old section index, map goes oldItem -> newItem +@property (nonatomic, strong, readonly) NSMutableArray *itemMappings; + +// array index is new section index, map goes newItem -> oldItem +@property (nonatomic, strong, readonly) NSMutableArray *reverseItemMappings; @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges; @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *originalInsertItemChanges; @@ -124,6 +130,10 @@ @implementation _ASHierarchyChangeSet { std::vector _newItemCounts; void (^_completionHandler)(BOOL finished); } +@synthesize sectionMapping = _sectionMapping; +@synthesize reverseSectionMapping = _reverseSectionMapping; +@synthesize itemMappings = _itemMappings; +@synthesize reverseItemMappings = _reverseItemMappings; - (instancetype)init { @@ -244,55 +254,121 @@ - (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType i - (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection { - ASDisplayNodeAssertNotNil(_deletedSections, @"Cannot call %@ before `markCompleted` returns.", NSStringFromSelector(_cmd)); - ASDisplayNodeAssertNotNil(_insertedSections, @"Cannot call %@ before `markCompleted` returns.", NSStringFromSelector(_cmd)); + return [self.sectionMapping integerForKey:oldSection]; +} + +- (NSUInteger)oldSectionForNewSection:(NSUInteger)newSection +{ + return [self.reverseSectionMapping integerForKey:newSection]; +} + +- (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 ([_deletedSections containsIndex:oldSection]) { - return NSNotFound; + if (_sectionMapping == nil) { + _sectionMapping = [ASIntegerMap mapForUpdateWithOldCount:_oldItemCounts.size() deleted:_deletedSections inserted:_insertedSections]; } + return _sectionMapping; +} - NSUInteger newIndex = oldSection - [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldSection)]; - newIndex += [_insertedSections as_indexChangeByInsertingItemsBelowIndex:newIndex]; - return newIndex; +- (ASIntegerMap *)reverseSectionMapping +{ + if (_reverseSectionMapping == nil) { + _reverseSectionMapping = [self.sectionMapping inverseMap]; + } + return _reverseSectionMapping; } -- (NSUInteger)oldSectionForNewSection:(NSUInteger)newSection +- (NSMutableArray *)itemMappings { [self _ensureCompleted]; - if ([_insertedSections containsIndex:newSection]) { - return NSNotFound; + + 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++; + } } - - NSInteger oldIndex = newSection - [_insertedSections as_indexChangeByInsertingItemsBelowIndex:newSection]; - oldIndex += [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldIndex)]; - return oldIndex; + 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]) { - return nil; - } + NSInteger oldItem = [[self reverseItemMappingInSection:newSection] integerForKey:indexPath.item]; + if (oldItem == NSNotFound) { + return nil; } - - // 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]; + return [NSIndexPath indexPathForItem:oldItem 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 @@ - (void)_sortAndCoalesceChangeArrays } [_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