From 1f994f23ac31aace4a6c08985c3ba7f7a79a36f8 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 30 Jun 2017 16:59:39 -0700 Subject: [PATCH 1/3] 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 77c10f78e..5f15f952a 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -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 */; }; @@ -788,7 +790,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 = ""; }; @@ -807,6 +809,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 = ""; }; @@ -1210,6 +1214,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */, + CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */, CC0F885E1E4280B800576FED /* _ASCollectionViewCell.h */, CC0F885D1E4280B800576FED /* _ASCollectionViewCell.m */, 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */, @@ -1318,7 +1324,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 +1719,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 */, @@ -2210,7 +2217,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 +2286,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 9265d8d0a..698143010 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -695,7 +695,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 dce5fd4cc..7eb263d79 100644 --- a/Source/Private/_ASHierarchyChangeSet.h +++ b/Source/Private/_ASHierarchyChangeSet.h @@ -18,6 +18,7 @@ #import #import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -91,7 +92,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 @@ -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)newItemCounts; diff --git a/Source/Private/_ASHierarchyChangeSet.mm b/Source/Private/_ASHierarchyChangeSet.mm index 4326b5e9e..e2e70c707 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 From a9b7341e52b645faa7f3d14ea16b772d7aad2cfe Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 5 Jul 2017 11:33:30 -0700 Subject: [PATCH 2/3] Add unit tests for ASIntegerMap --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + Source/Details/ASIntegerMap.h | 12 ++- Source/Details/ASIntegerMap.mm | 34 ++++++- Tests/ASIntegerMapTests.m | 117 ++++++++++++++++++++++ Tests/Common/ASTestCase.m | 2 +- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 Tests/ASIntegerMapTests.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 5f15f952a..58ac2eec4 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -401,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 */; }; @@ -877,6 +878,7 @@ CCE04B201E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "IGListAdapter+AsyncDisplayKit.h"; sourceTree = ""; }; CCE04B211E313EB9006AEBBB /* IGListAdapter+AsyncDisplayKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "IGListAdapter+AsyncDisplayKit.m"; sourceTree = ""; }; CCE04B2B1E314A32006AEBBB /* ASSupplementaryNodeSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASSupplementaryNodeSource.h; sourceTree = ""; }; + CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASIntegerMapTests.m; sourceTree = ""; }; D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = ""; }; @@ -1136,6 +1138,7 @@ CC034A0F1E60C9BF00626263 /* ASRectTableTests.m */, CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */, CC051F1E1D7A286A006434CB /* ASCALayerTests.m */, + CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.m */, CC8B05D71D73979700F54286 /* ASTextNodePerformanceTests.m */, CC8B05D41D73836400F54286 /* ASPerformanceTestContext.h */, CC8B05D51D73836400F54286 /* ASPerformanceTestContext.m */, @@ -2096,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 */, diff --git a/Source/Details/ASIntegerMap.h b/Source/Details/ASIntegerMap.h index dc82083dc..215e23332 100644 --- a/Source/Details/ASIntegerMap.h +++ b/Source/Details/ASIntegerMap.h @@ -28,8 +28,8 @@ AS_SUBCLASSING_RESTRICTED * If deleted and inserted are empty, returns the identity map. */ + (ASIntegerMap *)mapForUpdateWithOldCount:(NSInteger)oldCount - deleted:(NSIndexSet *)deleted - inserted:(NSIndexSet *)inserted; + deleted:(nullable NSIndexSet *)deleted + inserted:(nullable NSIndexSet *)inserted; /** * A singleton that maps each integer to itself. Its inverse is itself. @@ -48,6 +48,14 @@ AS_SUBCLASSING_RESTRICTED */ - (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. */ diff --git a/Source/Details/ASIntegerMap.mm b/Source/Details/ASIntegerMap.mm index fea08eb5b..4208b71a6 100644 --- a/Source/Details/ASIntegerMap.mm +++ b/Source/Details/ASIntegerMap.mm @@ -11,6 +11,7 @@ // #import "ASIntegerMap.h" +#import #import #import #import @@ -25,6 +26,7 @@ @implementation ASIntegerMap { std::unordered_map _map; BOOL _isIdentity; BOOL _isEmpty; + BOOL _immutable; // identity map and empty mape are immutable. } #pragma mark - Singleton @@ -36,6 +38,7 @@ + (ASIntegerMap *)identityMap dispatch_once(&onceToken, ^{ identityMap = [[ASIntegerMap alloc] init]; identityMap->_isIdentity = YES; + identityMap->_immutable = YES; }); return identityMap; } @@ -47,6 +50,7 @@ + (ASIntegerMap *)emptyMap dispatch_once(&onceToken, ^{ emptyMap = [[ASIntegerMap alloc] init]; emptyMap->_isEmpty = YES; + emptyMap->_immutable = YES; }); return emptyMap; } @@ -102,6 +106,16 @@ - (NSInteger)integerForKey:(NSInteger)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) { @@ -119,7 +133,13 @@ - (ASIntegerMap *)inverseMap - (id)copyWithZone:(NSZone *)zone { - return self; + if (_immutable) { + return self; + } + + auto newMap = [[ASIntegerMap allocWithZone:zone] init]; + newMap->_map = _map; + return newMap; } #pragma mark - Description @@ -153,4 +173,16 @@ - (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 diff --git a/Tests/ASIntegerMapTests.m b/Tests/ASIntegerMapTests.m new file mode 100644 index 000000000..7afdccf1c --- /dev/null +++ b/Tests/ASIntegerMapTests.m @@ -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 diff --git a/Tests/Common/ASTestCase.m b/Tests/Common/ASTestCase.m index bc17a564c..5bc720253 100644 --- a/Tests/Common/ASTestCase.m +++ b/Tests/Common/ASTestCase.m @@ -75,7 +75,7 @@ - (void)tearDown // 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]; } From 407c7dde14cd9e566464da349a3f127d18d90579 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 5 Jul 2017 11:34:19 -0700 Subject: [PATCH 3/3] Address nit --- Source/Details/ASIntegerMap.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Details/ASIntegerMap.h b/Source/Details/ASIntegerMap.h index 215e23332..2b94a8fce 100644 --- a/Source/Details/ASIntegerMap.h +++ b/Source/Details/ASIntegerMap.h @@ -22,7 +22,7 @@ AS_SUBCLASSING_RESTRICTED @interface ASIntegerMap : NSObject /** - * Creates an map based on the specified update to an array. + * 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. @@ -33,11 +33,15 @@ AS_SUBCLASSING_RESTRICTED /** * 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;