From 71d9f64535b2824d9c9c09552d14fe0782438de7 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 15:52:00 -0700 Subject: [PATCH 01/12] Carry over first-pass change set improvements --- AsyncDisplayKit.xcodeproj/project.pbxproj | 8 ++ AsyncDisplayKit/ASTableView.mm | 13 ++ AsyncDisplayKit/ASTableViewInternal.h | 3 + .../Details/ASChangeSetDataController.m | 20 ++- .../Details/NSIndexSet+ASHelpers.h | 23 ++++ .../Details/NSIndexSet+ASHelpers.m | 62 +++++++++ .../Private/_ASHierarchyChangeSet.h | 31 +++-- .../Private/_ASHierarchyChangeSet.m | 120 +++++++++++------- AsyncDisplayKitTests/ASTableViewThrashTests.m | 2 + .../TestResources/ASThrashTestRecordedCase | 2 +- 10 files changed, 224 insertions(+), 60 deletions(-) create mode 100644 AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h create mode 100644 AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 62c3baffc7..9fa6625a99 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -546,6 +546,8 @@ CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */; }; CC3B20901C3F892D00798563 /* ASBridgedPropertiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */; }; CC4981B31D1A02BE004E13CC /* ASTableViewThrashTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */; }; + CC4981BC1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; }; + CC4981BD1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */; }; CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC7FD9DF1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; }; @@ -937,6 +939,8 @@ CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASWeakSetTests.m; sourceTree = ""; }; CC3B208F1C3F892D00798563 /* ASBridgedPropertiesTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBridgedPropertiesTests.mm; sourceTree = ""; }; CC4981B21D1A02BE004E13CC /* ASTableViewThrashTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTableViewThrashTests.m; sourceTree = ""; }; + CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSIndexSet+ASHelpers.h"; sourceTree = ""; }; + CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSIndexSet+ASHelpers.m"; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; CC7FD9DD1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequest.m; sourceTree = ""; }; CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPhotosFrameworkImageRequestTests.m; sourceTree = ""; }; @@ -1229,6 +1233,8 @@ 058D09E1195D050800B7D73C /* Details */ = { isa = PBXGroup; children = ( + CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */, + CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m */, 9CFFC6BD1CCAC52B006A6476 /* ASEnvironment.mm */, 058D09E2195D050800B7D73C /* _ASDisplayLayer.h */, 058D09E3195D050800B7D73C /* _ASDisplayLayer.mm */, @@ -1634,6 +1640,7 @@ ACF6ED2D1B17843500DA7C62 /* ASRatioLayoutSpec.h in Headers */, AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */, 291B63FB1AA53A7A000A71B3 /* ASScrollDirection.h in Headers */, + CC4981BC1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h in Headers */, D785F6621A74327E00291744 /* ASScrollNode.h in Headers */, 058D0A7F195D05F900B7D73C /* ASSentinel.h in Headers */, 9C8221951BA237B80037F19A /* ASStackBaselinePositionedLayout.h in Headers */, @@ -2101,6 +2108,7 @@ ACF6ED4C1B17847A00DA7C62 /* ASInternalHelpers.mm in Sources */, 68FC85DF1CE29AB700EDD713 /* ASNavigationController.m in Sources */, ACF6ED251B17843500DA7C62 /* ASLayout.mm in Sources */, + CC4981BD1D1C7F65004E13CC /* NSIndexSet+ASHelpers.m in Sources */, DB55C2631C6408D6004EDCF5 /* _ASTransitionContext.m in Sources */, 92074A631CC8BA1900918F75 /* ASImageNode+tvOS.m in Sources */, 251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */, diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index c978bb9e5e..13e77c1a52 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -145,6 +145,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // Always set, whether ASCollectionView is created directly or via ASCollectionNode. @property (nonatomic, weak) ASTableNode *tableNode; +@property (nonatomic) BOOL test_enableSuperUpdateCallLogging; @end @implementation ASTableView @@ -974,6 +975,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super insertRowsAtIndexPaths]: %@", indexPaths); + } [super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; }); @@ -994,6 +998,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super deleteRowsAtIndexPaths]: %@", indexPaths); + } [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; }); @@ -1015,6 +1022,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super insertSections]: %@", indexSet); + } [super insertSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; }); @@ -1031,6 +1041,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; ASPerformBlockWithoutAnimation(preventAnimation, ^{ + if (self.test_enableSuperUpdateCallLogging) { + NSLog(@"-[super deleteSections]: %@", indexSet); + } [super deleteSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; }); diff --git a/AsyncDisplayKit/ASTableViewInternal.h b/AsyncDisplayKit/ASTableViewInternal.h index 93cdca0fcb..22ac11ff7e 100644 --- a/AsyncDisplayKit/ASTableViewInternal.h +++ b/AsyncDisplayKit/ASTableViewInternal.h @@ -34,4 +34,7 @@ */ - (instancetype)_initWithFrame:(CGRect)frame style:(UITableViewStyle)style dataControllerClass:(Class)dataControllerClass ownedByNode:(BOOL)ownedByNode; +/// Set YES and we'll log every time we call [super insertRows…] etc +@property (nonatomic) BOOL test_enableSuperUpdateCallLogging; + @end diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index efce00ad31..57fb476c79 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -14,6 +14,7 @@ #import "ASInternalHelpers.h" #import "_ASHierarchyChangeSet.h" #import "ASAssert.h" +#import "NSIndexSet+ASHelpers.h" #import "ASDataController+Subclasses.h" @@ -47,10 +48,18 @@ [super beginUpdates]; + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { + [super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; + } + for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { [super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; } + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { + [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; + } + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; } @@ -58,13 +67,12 @@ // TODO: Shouldn't reloads be processed before deletes, since deletes affect // the index space and reloads don't? for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { - [super reloadSections:change.indexSet withAnimationOptions:change.animationOptions]; + NSIndexSet *newIndexes = [change.indexSet as_indexesByMapping:^(NSUInteger idx) { + return [_changeSet newSectionForOldSection:idx]; + }]; + [super insertSections:newIndexes withAnimationOptions:change.animationOptions]; } - - for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { - [super reloadRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; - } - + for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { [super insertSections:change.indexSet withAnimationOptions:change.animationOptions]; } diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h new file mode 100644 index 0000000000..fc8fc6cafe --- /dev/null +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h @@ -0,0 +1,23 @@ +// +// NSIndexSet+ASHelpers.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 6/23/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#import + +@interface NSIndexSet (ASHelpers) + +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger))block; + +- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes; + +/// Returns all the item indexes from the given index paths that are in the given section. ++ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section; + +/// If you've got an old index, and you insert items using this index set, this returns the new index. +- (NSUInteger)as_indexByInsertingItemsBelowIndex:(NSUInteger)index; + +@end diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m new file mode 100644 index 0000000000..183ae959e4 --- /dev/null +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m @@ -0,0 +1,62 @@ +// +// NSIndexSet+ASHelpers.m +// AsyncDisplayKit +// +// Created by Adlai Holler on 6/23/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +@import UIKit; + +#import "NSIndexSet+ASHelpers.h" + +@implementation NSIndexSet (ASHelpers) + +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger))block +{ + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + [self enumerateIndexesUsingBlock:^(NSUInteger idx, __unused BOOL * _Nonnull stop) { + NSUInteger newIndex = block(idx); + if (newIndex != NSNotFound) { + [result addIndex:newIndex]; + } + }]; + return result; +} + +- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes +{ + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [indexes enumerateRangesInRange:range options:kNilOptions usingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [result addIndexesInRange:range]; + }]; + }]; + return result; +} + ++ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section +{ + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + for (NSIndexPath *indexPath in indexPaths) { + if (indexPath.section == section) { + [result addIndex:indexPath.item]; + } + } + return result; +} + +- (NSUInteger)as_indexByInsertingItemsBelowIndex:(NSUInteger)index +{ + __block NSUInteger newIndex = index; + [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { + if (idx <= newIndex) { + newIndex += 1; + } else { + *stop = YES; + } + }]; + return newIndex; +} + +@end diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h index 6ec3b4ca75..d46d0a3622 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -13,6 +13,8 @@ #import #import +NS_ASSUME_NONNULL_BEGIN + typedef NSUInteger ASDataControllerAnimationOptions; typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { @@ -34,11 +36,11 @@ typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { @property (nonatomic, readonly) ASDataControllerAnimationOptions animationOptions; /// Index paths are sorted descending for changeType .Delete, ascending otherwise -@property (nonatomic, strong, readonly) NSArray *indexPaths; +@property (nonatomic, strong, readonly) NSArray *indexPaths; @property (nonatomic, readonly) _ASHierarchyChangeType changeType; -+ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray *)changes ofType:(_ASHierarchyChangeType)changeType; ++ (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes ofType:(_ASHierarchyChangeType)changeType; @end @interface _ASHierarchyChangeSet : NSObject @@ -56,7 +58,15 @@ typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { @precondition The change set must be completed. @returns The new section index, or NSNotFound if the given section was deleted. */ -- (NSInteger)newSectionForOldSection:(NSInteger)oldSection; +- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection; + +/** + Get the index path after the update for the item at the given index path before the update. + + @precondition The change set must be completed. + @returns The new index path, or nil if the given item (or its section) was deleted. + */ +- (nullable NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath; @property (nonatomic, readonly) BOOL completed; @@ -77,13 +87,18 @@ typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { - Inserted sections, ascending order - Inserted items, ascending order */ -- (NSArray /*<_ASHierarchySectionChange *>*/ *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; -- (NSArray /*<_ASHierarchyItemChange *>*/ *)itemChangesOfType:(_ASHierarchyChangeType)changeType; +- (NSArray <_ASHierarchySectionChange *> *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; +- (NSArray <_ASHierarchyItemChange *> *)itemChangesOfType:(_ASHierarchyChangeType)changeType; + +/// Returns all item indexes affected by changes of the given type in the given section. +- (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section; - (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; - (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; - (void)reloadSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; -- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; -- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; -- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; +- (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options; @end + +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m index c89fc99332..9b32376faf 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -12,6 +12,7 @@ #import "_ASHierarchyChangeSet.h" #import "ASInternalHelpers.h" +#import "NSIndexSet+ASHelpers.h" @interface _ASHierarchySectionChange () - (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -23,7 +24,7 @@ + (void)sortAndCoalesceChanges:(NSMutableArray *)changes; /// Returns all the indexes from all the `indexSet`s of the given `_ASHierarchySectionChange` objects. -+ (NSMutableIndexSet *)allIndexesInChanges:(NSArray *)changes; ++ (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray *)changes; @end @interface _ASHierarchyItemChange () @@ -36,14 +37,29 @@ + (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections; @end +@implementation NSIndexSet (ASHierarchyHelpers) + +- (NSIndexSet *)intersectionWithIndexes:(NSIndexSet *)indexes +{ + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [indexes enumerateRangesInRange:range options:kNilOptions usingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [result addIndexesInRange:range]; + }]; + }]; + return result; +} + +@end + @interface _ASHierarchyChangeSet () -@property (nonatomic, strong, readonly) NSMutableArray *insertItemChanges; -@property (nonatomic, strong, readonly) NSMutableArray *deleteItemChanges; -@property (nonatomic, strong, readonly) NSMutableArray *reloadItemChanges; -@property (nonatomic, strong, readonly) NSMutableArray *insertSectionChanges; -@property (nonatomic, strong, readonly) NSMutableArray *deleteSectionChanges; -@property (nonatomic, strong, readonly) NSMutableArray *reloadSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *deleteItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *reloadItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *insertSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *deleteSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *reloadSectionChanges; @end @@ -103,24 +119,52 @@ } } -- (NSInteger)newSectionForOldSection:(NSInteger)oldSection +- (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section +{ + [self _ensureCompleted]; + NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; + for (_ASHierarchyItemChange *change in [self itemChangesOfType:changeType]) { + [result addIndexes:[NSIndexSet as_indexSetFromIndexPaths:change.indexPaths inSection:section]]; + } + return result; +} + +- (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection { [self _ensureCompleted]; if ([_deletedSections containsIndex:oldSection]) { return NSNotFound; } - __block NSInteger newIndex = oldSection - [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldSection)]; - [_insertedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - if (idx <= newIndex) { - newIndex += 1; - } else { - *stop = YES; - } - }]; + NSUInteger newIndex = oldSection - [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldSection)]; + newIndex = [_insertedSections as_indexByInsertingItemsBelowIndex:newIndex]; return newIndex; } +- (nullable NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath +{ + [self _ensureCompleted]; + // If section was deleted, nil + NSUInteger oldSection = indexPath.section; + NSUInteger newSection = [self newSectionForOldSection:oldSection]; + if (newSection == NSNotFound) { + return nil; + } + + NSUInteger newItem = indexPath.item; + NSIndexSet *deletedItemsInOldSection = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeDelete inSection:oldSection]; + newItem -= [deletedItemsInOldSection countOfIndexesInRange:NSMakeRange(0, newItem)]; + + for (_ASHierarchyItemChange *change in _deleteItemChanges) { + // If item was deleted, nil + if ([change.indexPaths containsObject:indexPath]) { + return nil; + } + } + + return [NSIndexPath indexPathForItem:newItem inSection:newSection]; +} + - (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options { [self _ensureNotCompleted]; @@ -184,10 +228,13 @@ [_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges]; [_ASHierarchySectionChange sortAndCoalesceChanges:_reloadSectionChanges]; - _deletedSections = [[_ASHierarchySectionChange allIndexesInChanges:_deleteSectionChanges] copy]; - _insertedSections = [[_ASHierarchySectionChange allIndexesInChanges:_insertSectionChanges] copy]; - _reloadedSections = [[_ASHierarchySectionChange allIndexesInChanges:_reloadSectionChanges] copy]; + _deletedSections = [[_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges] copy]; + _insertedSections = [[_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges] copy]; + _reloadedSections = [[_ASHierarchySectionChange allIndexesInSectionChanges:_reloadSectionChanges] copy]; + NSIndexSet *deletedAndReloaded = [_deletedSections intersectionWithIndexes:_reloadedSections]; + NSAssert(deletedAndReloaded.count == 0, @"Request to delete and reload the same section(s): %@", deletedAndReloaded); + // These are invalid old section indexes. NSMutableIndexSet *deletedOrReloaded = [_deletedSections mutableCopy]; [deletedOrReloaded addIndexes:_reloadedSections]; @@ -223,39 +270,22 @@ // - 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) { - __block NSUInteger section = indexPath.section; - __block NSUInteger row = indexPath.row; - - - // Update section number based on section insertions/deletions that are above the current section - section -= [_deletedSections countOfIndexesInRange:NSMakeRange(0, section)]; - [_insertedSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - if (idx <= section) { - section += 1; - } else { - *stop = YES; - } - }]; + 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)]; - row -= [indicesDeletedInSection countOfIndexesInRange:NSMakeRange(0, row)]; + 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)]; - [indicesInsertedInSection enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { - if (idx <= row) { - row += 1; - } else { - *stop = YES; - } - }]; + item = [indicesInsertedInSection as_indexByInsertingItemsBelowIndex:item]; //TODO: reuse the old indexPath object if section and row aren't changed - NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:row inSection:section]; + NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:item inSection:section]; [newIndexPaths addObject:newIndexPath]; } - // All reload changes are coalesced into deletes and inserts + // 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 _ASHierarchyItemChange *deleteItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:change.indexPaths animationOptions:change.animationOptions presorted:NO]; [_deleteItemChanges addObject:deleteItemChangeFromReloadChange]; @@ -346,7 +376,7 @@ [changes setArray:result]; } -+ (NSMutableIndexSet *)allIndexesInChanges:(NSArray *)changes ++ (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray<_ASHierarchySectionChange *> *)changes { NSMutableIndexSet *indexes = [NSMutableIndexSet indexSet]; for (_ASHierarchySectionChange *change in changes) { @@ -387,9 +417,9 @@ NSNumber *sectionKey = @(indexPath.section); NSMutableIndexSet *indexSet = sectionToIndexSetMap[sectionKey]; if (indexSet) { - [indexSet addIndex:indexPath.row]; + [indexSet addIndex:indexPath.item]; } else { - indexSet = [NSMutableIndexSet indexSetWithIndex:indexPath.row]; + indexSet = [NSMutableIndexSet indexSetWithIndex:indexPath.item]; sectionToIndexSetMap[sectionKey] = indexSet; } } diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 5d6fa8f637..ffdfa7eec8 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -8,6 +8,7 @@ @import XCTest; #import +#import "ASTableViewInternal.h" // Set to 1 to use UITableView and see if the issue still exists. #define USE_UIKIT_REFERENCE 0 @@ -477,6 +478,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; } ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:_update.oldData]; + ds.tableView.test_enableSuperUpdateCallLogging = YES; [self applyUpdate:_update toDataSource:ds]; [self verifyDataSource:ds]; } diff --git a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase index 9e8343590e..e9dc1e9cc0 100644 --- a/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase +++ b/AsyncDisplayKitTests/TestResources/ASThrashTestRecordedCase @@ -1 +1 @@ -YnBsaXN0MDDUAAEAAgADAAQABQAGDfYN91gkdmVyc2lvblgkb2JqZWN0c1kkYXJjaGl2ZXJUJHRvcBIAAYagrxEDRQAHAAgADwAtAC4ALwAwADEAMgAzADQANQA2ADcAOAA5AEAARgBdAGEAaABrAG4AcQB0AHcAegB9AIAAgwCGAIkAjACPAJIAlQCYAJsAngChAKYAqgCuAMUAyADLAM4A0QDUANcA2gDdAOAA4wDmAOkA7ADvAPIA9QD4APsA/gEBAQUBHAEfASIBJQEoASsBLgExATQBNwE6AT0BQAFDAUYBSQFMAU8BUgFVAVgBXAFzAXYBeQF8AX8BggGFAYgBiwGOAZEBlAGXAZoBnQGgAaMBpgGpAawBrwG1AbkBvgHDAdsB4AHjAeYB6AHqAewB7wHyAfQB9gH5AfwB/wICAgUCBwIKAg0CDwITAhYCGAIaAhwCIAIjAiYCKQIsAkMCSAJLAk4CUwJWAlkCXQJgAmQCZwJsAm8CcgJ4AnsCfgKBAoUCiAKNApACkwKXApoCngKhAqYCqQKsArECtAK3Ar0CwALDAsYCywLOAtEC2ALbAt4C4QLkAu4C8QL0AvcC+gL9AwADAwMHAwoDEAMTAxYDGQMeAyEDJAMnAz4DQgNZA1wDXwNiA2UDaANrA24DcQN0A3cDegN9A4ADgwOGA4kDjAOPA5IDlQOZA7ADswO2A7kDvAO/A8IDxQPIA8sDzgPRA9QD1wPaA90D4APjA+YD6QPsA/AEBwQKBA0EEAQTBBYEGQQcBB8EIgQlBCgEKwQuBDEENAQ3BDoEPQRABEMERwReBGEEZARnBGoEbQRwBHMEdgR5BHwEfwSCBIUEiASLBI4EkQSUBJcEmgSeBLUEuAS7BL4EwQTEBMcEygTNBNAE0wTWBNkE3ATfBOIE5QToBOsE7gTxBPUFDAUPBRIFFQUYBRsFHgUhBSQFJwUqBS0FMAUzBTYFOQU8BT8FQgVFBUgFTAVjBWYFaQVsBW8FcgV1BXgFewV+BYEFhAWHBYoFjQWQBZMFlgWZBZwFnwWjBboFvQXABcMFxgXJBcwFzwXSBdUF2AXbBd4F4QXkBecF6gXtBfAF8wX2BfoGEQYUBhcGGgYdBiAGIwYmBikGLAYvBjIGNQY4BjsGPgZBBkQGRwZKBk0GUQZoBmsGbgZxBnQGdwZ6Bn0GgAaDBoYGiQaMBo8GkgaVBpgGmwaeBqEGpAaoBr8GwgbFBsgGywbOBtEG1AbXBtoG3QbgBuMG5gbpBuwG7wbyBvUG+Ab7Bv8HFgcZBxwHHwciByUHKAcrBy4HMQc0BzcHOgc9B0AHQwdGB0kHTAdPB1IHVgdtB3AHcwd2B3kHfAd/B4IHhQeIB4sHjgeRB5QHlweaB50HoAejB6YHqQetB8QHxwfKB80H0AfTB9YH2QfcB98H4gflB+gH6wfuB/EH9Af3B/oH/QgACAQIGwgeCCEIJAgnCCoILQgwCDMINgg5CDwIPwhCCEUISAhLCE4IUQhUCFcIWwhyCHUIeAh7CH4IgQiECIcIigiNCJAIkwiWCJkInAifCKIIpQioCKsIrgiyCMkIzAjPCNII1QjYCNsI3gjhCOQI5wjqCO0I8AjzCPYI+Qj8CP8JAgkFCQkJIAkjCSYJKQksCS8JMgk1CTgJOwk+CUEJRAlHCUoJTQlQCVMJVglZCVwJYAl3CXoJfQmACYMJhgmJCYwJjwmSCZUJmAmbCZ4JoQmkCacJqgmtCbAJswm3Cc4J0QnUCdcJ2gndCeAJ4wnmCekJ7AnvCfIJ9Qn4CfsJ/goBCgQKBwoKCiIKJQo8Cj8KQgpcCl8KYgplCmgKfQqACpUKmAqwCrMKtgq6Cs0K0ArTCtYK2QrcCt8K4grlCugK6wruCvEK9Ar3CvoK/QsBCxcLGgsdCyALIwsmCykLLAsvCzILNQs4CzsLPgtBC0QLRwtKC00LUAtTC2wLbwtyC3ULjAuPC5ILrAuvC7ILtQu4C8wLzwvmC+kL7AvvDAgMCwwODBEMFQwoDCsMLgwxDDQMNww6DD0MQAxDDEYMSQxMDE8MUgxVDFgMWwxyDHUMeAx7DH4MgQyWDJkMnAyfDLYMuQy8DL8M1gzYDNsM3QzgDOMM5gzpDOwM7gzwDPIM9Az2DPgM+gz9DQANAw0GDQkNCw0ODRENFA0XDRoNMQ0zDTYNOQ08DT8NQg1FDUgNSw1NDU8NUQ1TDVYNWQ1cDV8NYQ1kDWcNag1tDXANcw11DXgNew1+DYENgw2bDZ8NpQ2oDaoNrQ2wDbUNug2+DcQNxw3MDdIN2Q3eDeIN5Q3oDe4N8lUkbnVsbNMACQAKAAsADAANAA5VX2RpY3RYX3ZlcnNpb25WJGNsYXNzgAIQAYEDRNMAEAARAAsAEgAf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rxAUAV4BXwFgAWEBYgFjAWQBZQFmAWcBaAFpAWoBawFsAW0BbgFvAXABcYBXgFiAWYBagFuAXIBdgF6AX4BggGGAYoBjgGSAZYBmgGeAaIBpgGqAJ9IAXgALAXQAYBEEToAT0gBeAAsBdwBgEQRPgBPSAF4ACwF6AGARBFCAE9IAXgALAX0AYBEEUYAT0gBeAAsBgABgEQRSgBPSAF4ACwGDAGARBFOAE9IAXgALAYYAYBEEVIAT0gBeAAsBiQBgEQRVgBPSAF4ACwGMAGARBFaAE9IAXgALAY8AYBEEV4AT0gBeAAsBkgBgEQRYgBPSAF4ACwGVAGARBFmAE9IAXgALAZgAYBEEWoAT0gBeAAsBmwBgEQRbgBPSAF4ACwGeAGARBFyAE9IAXgALAaEAYBEEXYAT0gBeAAsBpABgEQRegBPSAF4ACwGnAGARBF+AE9IAXgALAaoAYBEEYIAT0gBeAAsBrQBgEQRhgBPTAbAACwGxAbIBswG0XE5TUmFuZ2VDb3VudFtOU1JhbmdlRGF0YRACgG6AbNIBtgALAbcBuFdOUy5kYXRhRAYCEAGAbdIAYgBjAboBu11OU011dGFibGVEYXRhowG8Ab0AZ11OU011dGFibGVEYXRhVk5TRGF0YdIAYgBjAb8BwF8QEU5TTXV0YWJsZUluZGV4U2V0owHBAcIAZ18QEU5TTXV0YWJsZUluZGV4U2V0Wk5TSW5kZXhTZXTSABEACwHEAD+vEBUBxQHGAccByAHJAcoBywHMAc0BzgHPAdAB0QHSAdMB1AHVAdYB1wHYAdmAcIBxgHOAdIB1gHaAeIB5gHqAfIB9gH+AgICCgIOAhYCGgIeAiICKgIyAJ9QB3AALAbAB3QHeAbMADQANWk5TTG9jYXRpb25YTlNMZW5ndGgQAIBu0wGwAAsBsQGyAbMB4oBugHLSAbYACwHkAbhEAgEUAoBt0gGwAAsB3gGzgG7SAbAACwHeAbOAbtIBsAALAd4Bs4Bu0wGwAAsBsQGyAbMB7oBugHfSAbYACwHwAbhEEAETAYBt0gGwAAsB3gGzgG7SAbAACwHeAbOAbtMBsAALAbEBsgGzAfiAboB70gG2AAsB+gG4RAMBDAGAbdQB3AALAbAB3QH9AbMADQANEAeAbtMBsAALAbEBsgGzAgGAboB+0gG2AAsCAwG4RAwCFAGAbdIB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E9IAXgALBRMAYBECgoAT0gBeAAsFFgBgEQKDgBPSAF4ACwUZAGARAoSAE9IAXgALBRwAYBEChYAT0gBeAAsFHwBgEQKGgBPSAF4ACwUiAGARAoeAE9IAXgALBSUAYBECiIAT0gBeAAsFKABgEQKJgBPSAF4ACwUrAGARAoqAE9IAXgALBS4AYBECi4AT0gBeAAsFMQBgEQKMgBPSAF4ACwU0AGARAo2AE9IAXgALBTcAYBECjoAT0gBeAAsFOgBgEQKPgBPSAF4ACwU9AGARApCAE9IAXgALBUAAYBECkYAT0gBeAAsFQwBgEQKSgBPSAF4ACwVGAGARApOAE9MAQQBCAAsFSQVKAEWBAVIQW4Ao0gARAAsFTQA/rxAUBU4FTwVQBVEFUgVTBVQFVQVWBVcFWAVZBVoFWwVcBV0FXgVfBWAFYYEBU4EBVIEBVYEBVoEBV4EBWIEBWYEBWoEBW4EBXIEBXYEBXoEBX4EBYIEBYYEBYoEBY4EBZIEBZYEBZoAn0gBeAAsFZABgEQKUgBPSAF4ACwVnAGARApWAE9IAXgALBWoAYBECloAT0gBeAAsFbQBgEQKXgBPSAF4ACwVwAGARApiAE9IAXgALBXMAYBECmYAT0gBeAAsFdgBgEQKagBPSAF4ACwV5AGARApuAE9IAXgALBXwAYBECnIAT0gBeAAsFfwBgEQKdgBPSAF4ACwWCAGARAp6AE9IAXgALBYUAYBECn4AT0gBeAAsFiABgEQKggBPSAF4ACwWLAGARAqGAE9IAXgALBY4AYBECooAT0gBeAAsFkQBgEQKjgBPSAF4ACwWUAGARAqSAE9IAXgALBZcAYBECpYAT0gBeAAsFmgBgEQKmgBPSAF4ACwWdAGARAqeAE9MAQQBCAAsFoAWhAEWBAWgQXIAo0gARAAsFpAA/rxAUBaUFpgWnBagFqQWqBasFrAWtBa4FrwWwBbEFsgWzBbQFtQW2BbcFuIEBaYEBaoEBa4EBbIEBbYEBboEBb4EBcIEBcYEBcoEBc4EBdIEBdYEBdoEBd4EBeIEBeYEBeoEBe4EBfIAn0gBeAAsFuwBgEQKogBPSAF4ACwW+AGARAqmAE9IAXgALBcEAYBECqoAT0gBeAAsFxABgEQKrgBPSAF4ACwXHAGARAqyAE9IAXgALBcoAYBECrYAT0gBeAAsFzQBgEQKugBPS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ToECT4ECUIECUYDGgQJTgQJUgMeBAliAJ9IAXgALDHMAYBEEc4AT0gBeAAsMdgBgEQR0gBPSAF4ACwx5AGARBHWAE9IAXgALDHwAYBEEdoAT0wBBAEIACwx/CV4ARYEC7YAo0gARAAsMggA/rxASCWIJYwMbCWUJZglnCWgMiglqCWsMjQlsCW4JbwlxCXIJcwMcgQJbgQJcgMmBAl6BAl+BAmCBAmGBAu6BAmOBAmSBAu+BAmWBAmeBAmiBAmqBAmuBAmyAyoAn0gBeAAsMlwBgEQR3gBPSAF4ACwyaAGARBHiAE9MAQQBCAAsMnQm1AEWBAvGAKNIAEQALDKAAP68QFAm5CboJuwm8Cb0Mpgm+Cb8JwAnBCcIJwwnECcUJxgnHCcgJyQnLCcyBAnGBAnKBAnOBAnSBAnWBAvKBAnaBAneBAniBAnmBAnqBAnuBAnyBAn2BAn6BAn+BAoCBAoGBAoOBAoSAJ9IAXgALDLcAYBEEeYAT0wGwAAsBsQIQAbMMu4BugQL00gG2AAsMvQG4RgIBDQEQAoBt0gARAAsMwAA/rxAUDMEMwgzDDMQMxQzGDMcMyAzJDMoMywzMDM0MzgzPDNAM0QzSDNMM1IEC9oEC94EC+IEC+YEC+4EC/YEC/oEC/4EDAIEDAYEDAoEDA4EDBIEDBYEDB4EDCYEDCoEDC4EDDYEDD4An0gGwAAsB3gGzgG7UAdwACwGwAd0M2QGzAA0ADRANgG7SAbAACwHeAbOAbtMBsAALAbEBsgGzDN+AboEC+tIBtgALDOEBuEQDAQ0BgG3TAbAACwGxAbIBswzlgG6BAvzSAbYACwznAbhEAgEOAYBt1AHcAAsBsAHdDOoBswANAA0QDoBu0gGwAAsB3gGzgG7SAbAACwHeAbOAbtIBsAALAd4Bs4Bu0gGwAAsB3gGzgG7UAdwACwGwAd0CEAGzAA0ADYBu0gGwAAsB3gGzgG7SAbAACwHeAbOAbtMBsAALAbECEAGzDPyAboEDBtIBtgALDP4BuEYCAQoBDwGAbdMBsAALAbEBsgGzDQKAboEDCNIBtgALDQQBuEQEAQYBgG3UAdwACwGwAd0NBwGzAA0ADRATgG7SAbAACwHeAbOAbtMBsAALAbECEAGzDQ2AboEDDNIBtgALDQ8BuEYEAQYBEQKAbdMBsAALAbECHQGzDROAboEDDtIB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FUYVSBVKFUwVThVQFVkVXBVeFWcVahVsFXUVeBV6FYMVhhWIFZEVoBWiFaQVphWoFaoVrBWuFbAVuRW8Fb4VxxXKFcwV1RXYFdoV4xXmFegV8RX0FfYV/xYCFgQWDRYQFhIWGxYeFiAWIhYrFi4WMBY5FkAWQhZEFkYWSBZRFlQWVhZfFmIWZBZtFnAWchZ7FoAWghaEFoYWjxaSFpQWnRagFqIWqxasFq4WtxbiFuQW5hboFusW7hbxFvQW9xb6Fv0XABcDFwYXCRcMFw8XEhcVFxgXGxcdFyoXLBcuFzAXORdkF2YXaBdqF2wXbhdwF3IXdBd2F3gXehd8F34XgBeCF4QXhheIF4oXjBeOF5cXmhecF6UXqBeqF7MXthe4F8EXxBfGF88X0hfUF90X4BfiF+sX7hfwF/kX/Bf+GAcYChgMGBUYGBgaGCMYJhgoGDEYNBg2GD8YQhhEGE0YUBhSGFsYXhhgGGkYbBhuGHcYehh8GIUYiBiKGJMYlhiYGKEYpBimGLMYtRi3GLkYwhjtGO8Y8RjzGPUY9xj5GPsY/Rj/GQEZAxkFGQcZCRkLGQ0ZDxkRGRMZFRkXGSAZIxklGS4ZMRkzGTwZPxlBGUoZTRlPGVgZWxldGWYZaRlrGXQZdxl5GYIZhRmHGZAZkxmVGZ4ZoRmjGawZrxmxGboZvRm/GcgZyxnNGdYZ2RnbGeQZ5xnpGfIZ9Rn3GgAaAxoFGg4aERoTGhwaHxohGioaLRovGjwaPhpAGkIaSxp2Gngaehp8Gn4agBqDGoYaiRqMGo8akhqVGpgamxqeGqEapBqnGqoarRqvGrgauxq9GsYayRrLGtQa1xrZGuIa5RrnGvAa8xr1Gv4bARsDGwwbDxsRGxobHRsfGygbKxstGzYbORs7G0QbRxtJG1IbVRtXG2AbYxtlG24bcRtzG3wbfxuBG4objRuPG5gbmxudG6YbqRurG7Qbtxu5G8IbxRvHG9Qb1xvZG9sb5BwPHBIcFRwYHBscHhwhHCQcJxwqHC0cMBwzHDYcORw8HD8cQhxFHEgcSxxNHFYcWRxbHGQcZxxpHHIcdRx3HIAcgxyFHI4ckRyTHJwcnxyhHKocrRyvHLgcuxy9HMYcyRzLHNQc1xzZHOIc5RznHPAc8xz1HP4dAR0DHQwdDx0RHRodHR0fHSgdKx0tHTYdOR07HUQdRx1JHVIdVR1XHWAdYx1lHXIddR13HXkdgh2tHbAdsx22HbkdvB2/HcIdxR3IHcsdzh3RHdQd1x3aHd0d4B3jHeYd6R3rHfQd9x35HgIeBR4HHhAeEx4VHh4eIR4jHiweLx4xHjoePR4/HkgeSx5NHlYeWR5bHmQeZx5pHnIedR53HoAegx6FHo4ekR6THpwenx6hHqoerR6vHrgeux69HsYeyR7LHtQe1x7ZHuIe5R7nHvAe8x71Hv4fAR8DHxAfEx8VHxcfIB9LH04fUR9UH1cfWh9dH2AfYx9mH2kfbB9vH3IfdR94H3sffh+BH4Qfhx+JH5IflR+XH6Afox+lH64fsR+zH7wfvx/BH8ofzR/PH9gf2x/dH+Yf6R/rH/Qf9x/5IAIgBSAHIBAgEyAVIB4gISAjICwgLyAxIDogPSA/IEggSyBNIFYgWSBbIGQgZyBpIHIgdSB3IIAggyCFII4gkSCTIJwgnyChIK4gsSCzILUgviDpIOwg7yDyIPUg+CD7IP4hASEEIQchCiENIRAhEyEWIRkhHCEfISIhJSEnITAhMyE1IT4hQSFDIUwhTyFRIVohXSFfIWghayFtIXYheSF7IYQhhyGJIZIhlSGXIaAhoyGlIa4hsSGzIbwhvyHBIcohzSHPIdgh2yHdIeYh6SHrIfQh9yH5IgIiBSIHIhAiEyIVIh4iISIjIiwiLyIxIjoiPSI/IkwiTyJRIlMiXCKHIooijSKQIpMiliKZIpwinyKiIqUiqCKrIq4isSK0IrciuiK9IsAiwyLFIs4i0SLTItwi3yLhIuoi7SLvIvgi+yL9IwYjCSMLIxQjFyMZIyIjJSMnIzAjMyM1Iz4jQSNDI0wjTyNRI1ojXSNfI2gjayNtI3YjeSN7I4QjhyOJI5IjlSOXI6AjoyOlI64jsSOzI7wjvyPBI8ojzSPPI9gj2yPdI+oj7SPvI/Ej+iQlJCgkKyQuJDEkNCQ3JDokPSRAJEMkRiRJJEwkTyRSJFUkWCRbJF4kYSRjJGwkbyRxJHokfSR/JIgkiySNJJYkmSSbJKQkpySpJLIktSS3JMAkwyTFJM4k0STTJNwk3yThJOok7STvJPgk+yT9JQYlCSULJRQlFyUZJSIlJSUnJTAlMyU1JT4lQSVDJUwlTyVRJVolXSVfJWglayVtJXYleSV7JYgliyWNJY8lmCXDJcYlySXMJc8l0iXVJdgl2yXeJeEl5CXnJeol7SXwJfMl9iX5Jfwl/yYBJgomDSYPJhgmGyYdJiYmKSYrJjQmNyY5JkImRSZHJlAmUyZVJl4mYSZjJmwmbyZxJnomfSZ/JogmiyaNJpYmmSabJqQmpyapJrImtSa3JsAmwybFJs4m0SbTJtwm3ybhJuom7SbvJvgm+yb9JwYnCScLJxQnFycZJyYnKScrJy0nNidhJ2QnZydqJ20ncCdzJ3YneSd8J38ngieFJ4gniyeOJ5EnlCeXJ5onnSefJ6gnqyetJ7YnuSe7J8QnxyfJJ9In1SfXJ+An4yflJ+4n8SfzJ/wn/ygBKAooDSgPKBgoGygdKCYoKSgrKDQoNyg5KEIoRShHKFAoUyhVKF4oYShjKGwobyhxKHoofSh/KIgoiyiNKJYomSibKKQopyipKLIotSi3KMQoxyjJKMso1Cj/KQIpBSkIKQspDikRKRQpFykaKR0pICkjKSYpKSksKS8pMik1KTgpOyk9KUYpSSlLKVQpVylZKWIpZSlnKXApcyl1KX4pgSmDKYwpjymRKZopnSmfKagpqymtKbYpuSm7KcQpxynJKdIp1SnXKeAp4ynlKe4p8SnzKfwp/yoBKgoqDSoPKhgqGyodKiYqKSorKjQqNyo5KkIqRSpHKlAqUypVKmIqZSpnKmkqciqdKqAqoyqmKqkqrCqvKrIqtSq4KrsqvirBKsQqxyrKKs0q0CrTKtYq2SrbKuQq5yrpKvIq9Sr3KwArAysFKw4rESsTKxwrHyshKyorLSsvKzgrOys9K0YrSStLK1QrVytZK2IrZStnK3Arcyt1K34rgSuDK4wrjyuRK5ornSufK6grqyutK7YruSu7K8QrxyvJK9Ir1SvXK+Ar4yvlK+4r8SvzLAAsAywFLAcsECw7LD4sQSxELEcsSixNLFAsUyxWLFksXCxfLGIsZSxoLGssbixxLHQsdyx5LIIshSyHLJAskyyVLJ4soSyjLKwsryyxLLosvSy/LMgsyyzNLNYs2SzbLOQs5yzpLPIs9Sz3LQAtAy0FLQ4tES0TLRwtHy0hLSotLS0vLTgtOy09LUYtSS1LLVQtVy1ZLWItZS1nLXAtcy11LX4tgS2DLYwtjy2RLZ4toS2jLaUtri3ZLdwt3y3iLeUt6C3rLe4t8S30Lfct+i39LgAuAy4GLgkuDC4PLhIuFS4XLiAuIy4lLi4uMS4zLjwuPy5BLkouTS5PLlguWy5dLmYuaS5rLnQudy55LoIuhS6HLpAuky6VLp4uoS6jLqwury6xLrouvS6/Lsguyy7NLtYu2S7bLuQu5y7pLvIu9S73LwAvAy8FLw4vES8TLxwvHy8hLyovLS8vLzwvPy9BL0MvTC93L3ovfS+AL4Mvhi+JL4wvjy+SL5UvmC+bL54voS+kL6cvqi+tL7Avsy+1L74vwS/DL8wvzy/RL9ov3S/fL+gv6y/tL/Yv+S/7MAQwBzAJMBIwFTAXMCAwIzAlMC4wMTAzMDwwPzBBMEowTTBPMFgwWzBdMGYwaTBrMHQwdzB5MIIwhTCHMJAwkzCVMJ4woTCjMKwwrzCxMLowvTC/MMgwyzDNMNow3TDfMOEw6jEVMRgxGzEeMSExJDEnMSoxLTEwMTMxNjE5MTwxPzFCMUUxSDFLMU4xUTFTMVwxXzFhMWoxbTFvMXgxezF9MYYxiTGLMZQxlzGZMaIxpTGnMbAxszG1Mb4xwTHDMcwxzzHRMdox3THfMegx6zHtMfYx+TH7MgQyBzIJMhIyFTIXMiAyIzIlMi4yMTIzMjwyPzJBMkoyTTJPMlgyWzJdMmYyaTJrMngyezJ9Mn8yiDKzMrYyuTK8Mr8ywjLFMsgyyzLOMtEy1DLXMtoy3TLgMuMy5jLpMuwy7zLxMvoy/TL/MwgzCzMNMxYzGTMbMyQzJzMpMzIzNTM3M0AzQzNFM04zUTNTM1wzXzNhM2ozbTNvM3gzezN9M4YziTOLM5QzlzOZM6IzpTOnM7AzszO1M74zwTPDM8wzzzPRM9oz3TPfM+gz6zPtM/Yz+TP7NAQ0BzQJNBY0GTQbNB00JjRRNFQ0VzRaNF00YDRjNGY0aTRsNG80cjR1NHg0ezR+NIE0hDSHNIo0jTSPNJg0mzSdNKY0qTSrNLQ0tzS5NMI0xTTHNNA00zTVNN404TTjNOw07zTxNPo0/TT/NQg1CzUNNRY1GTUbNSQ1JzUpNTI1NTU3NUA1QzVFNU41UTVTNVw1XzVhNWo1bTVvNXg1ezV9NYY1iTWLNZQ1lzWZNaI1pTWnNbQ1tzW5Nbs1xDXvNfI19TX4Nfs1/jYBNgQ2BzYKNg02EDYTNhY2GTYcNh82IjYlNig2KzYtNjY2OTY7NkQ2RzZJNlI2VTZXNmA2YzZlNm42cTZzNnw2fzaBNoo2jTaPNpg2mzadNqY2qTarNrQ2tza5NsI2xTbHNtA20zbVNt424TbjNuw27zbxNvo2/Tb/Nwg3CzcNNxY3GTcbNyQ3JzcpNzI3NTc3N0A3QzdFN043ezd+N4E3gzeGN4k3jDePN5I3lTeYN5s3njehN6M3pjepN6s3rTewN7M3tje4N8U3yDfKN9M3/jgBOAM4BTgHOAk4CzgNOA84ETgTOBU4FzgZOBs4HTgfOCE4IzglOCc4KTgyODU4NzhEOEc4SThSOIM4hTiHOIo4jDiOOJA4kziWOJk4nDifOKE4pDinOKo4rTiwOLM4tji5OLw4vzjCOMQ4zTjQONI42zjeOOA46TjsOO44+zj+OQA5CTkwOTM5Njk5OTw5PzlBOUQ5RzlKOU05UDlTOVY5WTlcOV85YjllOWc5dDl3OXk5gjmpOaw5rjmxObQ5tzm6Ob05wDnDOcY5yTnMOc850jnVOdg52jndOd857DnvOfE5+jonOio6LTowOjM6NTo4Ojo6PTpAOkM6RTpIOks6TjpROlQ6VzpaOl06YDpjOmU6bjpxOnM6fDp/OoE6jjqROpM6lTqeOsE6xDrHOso6zTrQOtM61jrZOtw63zriOuU66DrrOu468TrzOvw6/zsBOwo7DTsPOxg7GzsdOyY7KTsrOzQ7Nzs5O0I7RTtHO1A7UztVO147YTtjO2w7bztxO3o7fTt/O4g7izuNO5Y7mTubO6Q7pzupO7I7tTu3O8A7wzvFO8470TvTO+A74zvlO+c78DwZPBw8HzwiPCU8KDwrPC48MTw0PDc8Ojw9PEA8QzxGPEk8TDxPPFI8VDxdPGA8YjxrPG48cDx5PHw8fjyHPIo8jDyVPJg8mjyjPKY8qDyxPLQ8tjy/PMI8xDzNPNA80jzbPN484DzpPOw87jz3PPo8/D0FPQg9Cj0TPRY9GD0hPSQ9Jj0vPTI9ND09PUA9Qj1LPU49UD1ZPVw9Xj1rPW49cD15Pag9qz2uPbE9tD23Pbo9vT3APcI9xT3IPcs9zj3RPdQ91z3aPd094D3jPeY96T3rPfQ99z35PgI+BT4HPhQ+Fz4ZPiI+TT5QPlM+VT5YPls+Xj5hPmQ+Zz5qPm0+cD5zPnU+eD57Pn4+gT6EPoc+iT6SPpU+lz6kPqc+qT6yPuM+5j7pPuw+7z7yPvU++D77Pv4/AT8EPwY/CT8MPw8/Ej8VPxg/Gz8dPyA/Iz8mPyg/MT80PzY/Pz9CP0Q/TT9QP1I/Xz9iP2Q/bT+SP5U/mD+bP54/oT+kP6c/qj+tP68/sj+1P7g/uz++P8A/wz/FP9I/1T/XP+BAC0AOQBFAFEAXQBpAHEAfQCJAJEAnQClALEAvQDJANUA4QDtAPkBBQENARUBOQFFAU0BcQF9AYUBuQHFAc0B8QKtArkCwQLNAtkC4QLtAvkDBQMRAx0DKQMxAzkDRQNRA10DaQN1A4EDiQORA5kDoQPFA9ED2QP9BAkEEQQ1BEEESQR9BIkEkQSZBL0FSQVVBWEFbQV5BYUFkQWdBakFtQXBBc0F2QXlBfEF/QYJBhEGNQZBBkkGbQZ5BoEGpQaxBrkG3QbpBvEHFQchBykHTQdZB2EHhQeRB5kHvQfJB9EH9QgBCAkILQg5CEEIZQhxCHkInQipCLEI1QjhCOkJDQkZCSEJRQlRCVkJfQmJCZEJxQnRCdkJ/QqpCrUKwQrNCtkK5QrxCvkLBQsRCx0LKQs1C0ELTQtZC2ELbQt5C4ELjQuVC7kLxQvNC/EL/QwFDCkMNQw9DGEMbQx1DKkMtQy9DOENfQ2JDZUNnQ2pDbUNwQ3NDdkN5Q3xDf0OCQ4VDiEOLQ45DkUOTQ5VDnkOhQ6NDrEOvQ7FDvkPBQ8NDzEP3Q/pD/UQARANEBkQJRAxED0QSRBVEGEQbRB5EIUQkRCdEKkQtRDBEM0Q1RD5EQURDRFBEUkRVRF5EZURnRHBEm0SeRKFEpESnRKpErUSwRLNEtkS5RLxEv0TCRMVEyETLRM5E0UTURNdE2UTiRORE9UT3RPlFAkUERRFFE0UWRR9FJEUmRTNFNUU4RUFFRkVIRVlFW0VdRWZFaEVxRXNFfEV+RYdFiUWaRZxFpUWnRbBFskW/RcFFxEXNRdRF1kXjReVF6EXxRfZF+EYJRgtGDUYWRhhGJUYnRipGM0Y6RjxGSUZLRk5GV0ZgRmJGc0Z1RndGgEarRq5GsUa0RrdGuka9RsBGw0bGRslGzEbPRtJG1UbYRttG3kbhRuRG50bpRvJG9EcBRwNHBkcPRxRHFkcnRylHK0c8Rz5HQEdNR09HUkdbR2BHYkdvR3FHdEd9R4RHhkePR5FHmkecR61Hr0e4R7pHx0fJR8xH1UfaR9xH6UfrR+5H90f8R/5IB0gJSBZIGEgbSCRIKUgrSDhIOkg9SEZIT0hRSF5IYEhjSGxIdUh3SIBIgkiPSJFIlEidSKRIpkizSLVIuEjBSMZIyEjRSNNI3EkJSQxJD0kSSRVJGEkbSR5JIUkkSSdJKkktSTBJM0k2STlJPEk/SUJJRUlISUpJU0lWSVlJW0lkSWtJbklxSXRJdkl/SYBJg0mMSZFJmkmbSZ1JpkmnSalJskm3SbpJvUm/SchJzUnQSdNJ1UneSeFJ5EnmSe9J9kn5SfxJ/0oBSgpKC0oNShZKG0oeSiFKI0osSjNKNko5SjxKPkpHSlBKU0pWSllKXEpeSmdKbEpvSnJKdEp9SoBKg0qFSpJKlEqXSqBKp0qpSrJKuUq8Sr9KwkrESs1K2krfSuxK9UsESwlLGEsqSy9LNAAAAAAAAAICAAAAAAAADfoAAAAAAAAAAAAAAAAAAEs2 \ No newline at end of file +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BvwHBAcMBxQHHAckBywHeAfcCDQIcAiQCKQJCAlcCbQJ7ApMCpwKwArECswK8AscC0ALfAuYC9QL9AwYDFwMiAy8DOAM6AzwDRQNZA2ADdAN/A4gDkQOTA5UDlwOZA5sDrAOuA7ADuQO7A70DxgPIA9kD2wPdA+YD8QPzA/UD9wP5A/sD/QQGBAcECQQSBBMEFQQeBB8EIQQqBCsELQQ2BDcEOQRCBE0ETwRRBFMEVQRXBFkEZgRsBHYEeAR7BH0EhgSRBJMElQSXBJkEmwSdBKYErQSwBLIEuwTOBNME5gTvBPIE9AT9BQAFAgULBQ4FEAUZBRwFHgUnBT0FQgVYBWUFZwVqBWwFdQWABYIFhAWGBYgFigWMBZUFmAWaBaMFpgWoBbEFtAW2Bb8FwgXEBc0F0AXSBd8F4QXkBeYF7wX6BfwF/gYABgIGBAYGBg8GEgYUBh0GIAYiBisGLgYwBjkGPAY+BkcGSgZMBlkGWwZeBmAGaQZ0BnYGeAZ6BnwGfgaABokGjAaOBpcGmgacBqUGqAaqBrMGtga4BsEGxAbGBtMG1QbYBtoG4wbuBvAG8gb0BvYG+Ab6BwMHBgcIBxEHFAcWBx8HIgckBy0HMAcyBzsHPgdAB0kHTgdXB2AHYgdkB2YHaAdqB3cHeQd7B4QHkQeTB5UHlweZB5sHnQefB6gHqwetB7oHvAe/B8EHygfVB9cH2QfbB90H3wfhB+oH7QfvB/gH+wf9CAYICQgLCBQIFwgZCCIIJQgnCDQINgg4CEEITAhOCFAIUghUCFYIWAhlCGcIaQhyCH8IgQiDCIUIhwiJCIsIjQiWCJkImwikCKYIrwi6CLwIvgjACMIIxAjGCM8I0QjaCNwI5QjnCPAI8gj7CP0JBgkRCRMJFQkXCRkJGwkdCSYJKAkxCTMJPAk+CUcJSQlSCVQJXQlmCWgJaglsCW4JcAl5CXwJfgmACYkJigmMCZUJlgmYCaEJpAmmCagJuQm7CcQJxwnJCcsJ1AnhCeYJ8wn8CgsKEAofCjEKNgo7AAAAAAAAAgIAAAAAAAABrAAAAAAAAAAAAAAAAAAACj0= \ No newline at end of file From 35056f708b0d58fdd243f6195f0970dc273fb2ab Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 16:57:09 -0700 Subject: [PATCH 02/12] [ASDataController] Improve update handling --- AsyncDisplayKit/ASTableView.mm | 6 ++ .../Details/ASChangeSetDataController.m | 24 ++--- .../Details/ASCollectionDataController.mm | 48 --------- .../Details/ASDataController+Subclasses.h | 44 --------- AsyncDisplayKit/Details/ASDataController.mm | 70 +------------ .../Details/NSIndexSet+ASHelpers.h | 4 +- .../Details/NSIndexSet+ASHelpers.m | 14 +++ .../Private/_ASHierarchyChangeSet.h | 4 +- .../Private/_ASHierarchyChangeSet.m | 99 ++++++++++--------- AsyncDisplayKitTests/ASTableViewThrashTests.m | 15 ++- 10 files changed, 95 insertions(+), 233 deletions(-) diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 13e77c1a52..c0dfd1fc15 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -456,18 +456,21 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } [_dataController insertSections:sections withAnimationOptions:animation]; } - (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } [_dataController deleteSections:sections withAnimationOptions:animation]; } - (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } [_dataController reloadSections:sections withAnimationOptions:animation]; } @@ -480,18 +483,21 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } [_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } [_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation { ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } [_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animation]; } diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index 57fb476c79..7802a6c1d5 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -48,30 +48,16 @@ [super beginUpdates]; - for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeReload]) { - [super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; - } + NSAssert([_changeSet itemChangesOfType:_ASHierarchyChangeTypeReload].count == 0, @"Expected reload item changes to have been converted into insert/deletes."); + NSAssert([_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload].count == 0, @"Expected reload section changes to have been converted into insert/deletes."); for (_ASHierarchyItemChange *change in [_changeSet itemChangesOfType:_ASHierarchyChangeTypeDelete]) { [super deleteRowsAtIndexPaths:change.indexPaths withAnimationOptions:change.animationOptions]; } - for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { - [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; - } - for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { [super deleteSections:change.indexSet withAnimationOptions:change.animationOptions]; } - - // TODO: Shouldn't reloads be processed before deletes, since deletes affect - // the index space and reloads don't? - for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeReload]) { - NSIndexSet *newIndexes = [change.indexSet as_indexesByMapping:^(NSUInteger idx) { - return [_changeSet newSectionForOldSection:idx]; - }]; - [super insertSections:newIndexes withAnimationOptions:change.animationOptions]; - } for (_ASHierarchySectionChange *change in [_changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { [super insertSections:change.indexSet withAnimationOptions:change.animationOptions]; @@ -123,7 +109,8 @@ if ([self batchUpdating]) { [_changeSet reloadSections:sections animationOptions:animationOptions]; } else { - [super reloadSections:sections withAnimationOptions:animationOptions]; + [super deleteSections:sections withAnimationOptions:animationOptions]; + [super insertSections:sections withAnimationOptions:animationOptions]; } } @@ -166,7 +153,8 @@ if ([self batchUpdating]) { [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; } else { - [super reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [super deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [super insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; } } diff --git a/AsyncDisplayKit/Details/ASCollectionDataController.mm b/AsyncDisplayKit/Details/ASCollectionDataController.mm index b54583c2a2..df0e7d592b 100644 --- a/AsyncDisplayKit/Details/ASCollectionDataController.mm +++ b/AsyncDisplayKit/Details/ASCollectionDataController.mm @@ -115,30 +115,6 @@ } } -- (void)prepareForReloadSections:(NSIndexSet *)sections -{ - for (NSString *kind in [self supplementaryKinds]) { - NSMutableArray *contexts = [NSMutableArray array]; - [self _populateSupplementaryNodesOfKind:kind withSections:sections mutableContexts:contexts]; - _pendingContexts[kind] = contexts; - } -} - -- (void)willReloadSections:(NSIndexSet *)sections -{ - NSArray *keys = _pendingContexts.allKeys; - for (NSString *kind in keys) { - NSMutableArray *contexts = _pendingContexts[kind]; - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet([self editingNodesOfKind:kind], sections); - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - // reinsert the elements - [self batchLayoutNodesFromContexts:contexts ofKind:kind completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; - }]; - [_pendingContexts removeObjectForKey:kind]; - } -} - - (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection { for (NSString *kind in [self supplementaryKinds]) { @@ -187,30 +163,6 @@ } } -- (void)prepareForReloadRowsAtIndexPaths:(NSArray *)indexPaths -{ - for (NSString *kind in [self supplementaryKinds]) { - NSMutableArray *contexts = [NSMutableArray array]; - [self _populateSupplementaryNodesOfKind:kind atIndexPaths:indexPaths mutableContexts:contexts]; - _pendingContexts[kind] = contexts; - } -} - -- (void)willReloadRowsAtIndexPaths:(NSArray *)indexPaths -{ - NSArray *keys = _pendingContexts.allKeys; - for (NSString *kind in keys) { - NSMutableArray *contexts = _pendingContexts[kind]; - - [self deleteNodesOfKind:kind atIndexPaths:indexPaths completion:nil]; - // reinsert the elements - [self batchLayoutNodesFromContexts:contexts ofKind:kind completion:^(NSArray *nodes, NSArray *indexPaths) { - [self insertNodes:nodes ofKind:kind atIndexPaths:indexPaths completion:nil]; - }]; - [_pendingContexts removeObjectForKey:kind]; - } -} - - (void)_populateSupplementaryNodesOfKind:(NSString *)kind withMutableContexts:(NSMutableArray *)contexts { id environment = [self.environmentDelegate dataControllerEnvironment]; diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h index d837540362..099a9bfe45 100644 --- a/AsyncDisplayKit/Details/ASDataController+Subclasses.h +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -128,28 +128,6 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray *nodes, NS */ - (void)willDeleteSections:(NSIndexSet *)sections; -/** - * Notifies the subclass to perform any work needed before the given sections will be reloaded. - * - * @discussion This method will be performed before the data controller enters its editing queue, usually on the main - * thread. The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or - * data stores before entering into editing the backing store on a background thread. - * - * @param sections Indices of sections to be reloaded - */ -- (void)prepareForReloadSections:(NSIndexSet *)sections; - -/** - * Notifies the subclass that the data controller will reload the sections in the given index set - * - * @discussion This method will be performed on the data controller's editing background queue before the parent's - * concrete implementation. This is a great place to perform any additional transformations like supplementary views - * or header/footer nodes. - * - * @param sections Indices of sections to be reloaded - */ -- (void)willReloadSections:(NSIndexSet *)sections; - /** * Notifies the subclass that the data controller will move a section to a new position * @@ -206,26 +184,4 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray *nodes, NS */ - (void)willDeleteRowsAtIndexPaths:(NSArray *)indexPaths; -/** - * Notifies the subclass to perform any work needed before the given rows will be reloaded. - * - * @discussion This method will be performed before the data controller enters its editing queue, usually on the main - * thread. The data source is locked at this point and accessing it is safe. Use this method to set up any nodes or - * data stores before entering into editing the backing store on a background thread. - * - * @param indexPaths Index paths for the rows to be reloaded. - */ -- (void)prepareForReloadRowsAtIndexPaths:(NSArray *)indexPaths; - -/** - * Notifies the subclass that the data controller will reload the rows at the given index paths. - * - * @discussion This method will be performed on the data controller's editing background queue before the parent's - * concrete implementation. This is a great place to perform any additional transformations like supplementary views - * or header/footer nodes. - * - * @param indexPaths Index paths for the rows to be reloaded. - */ -- (void)willReloadRowsAtIndexPaths:(NSArray *)indexPaths; - @end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 86967fc99a..74869de0d7 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -65,6 +65,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; if (!(self = [super init])) { return nil; } + ASDisplayNodeAssert(![self isMemberOfClass:[ASDataController class]], @"ASDataController is an abstract class and should not be instantiated. Instantiate a subclass instead."); _completedNodes = [NSMutableDictionary dictionary]; _editingNodes = [NSMutableDictionary dictionary]; @@ -661,29 +662,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - reloadSections: %@", sections); - - [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - - NSArray *contexts= [self _populateFromDataSourceWithSectionIndexSet:sections]; - - [self prepareForReloadSections:sections]; - - [_editingTransactionQueue addOperationWithBlock:^{ - [self willReloadSections:sections]; - - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes[ASDataControllerRowNodeKind], sections); - - LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForTwoDimensionalArray(_editingNodes[ASDataControllerRowNodeKind])); - - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - - // reinsert the elements - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }]; - }]; + ASDisplayNodeAssert(NO, @"ASDataController does not support %@. Call this on ASChangeSetDataController the reload will be broken into delete & insert.", NSStringFromSelector(_cmd)); } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -781,16 +760,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Optional template hook for subclasses (See ASDataController+Subclasses.h) } -- (void)prepareForReloadRowsAtIndexPaths:(NSArray *)indexPaths -{ - // Optional template hook for subclasses (See ASDataController+Subclasses.h) -} - -- (void)willReloadRowsAtIndexPaths:(NSArray *)indexPaths -{ - // Optional template hook for subclasses (See ASDataController+Subclasses.h) -} - #pragma mark - Row Editing (External API) - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions @@ -853,40 +822,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self performEditCommandWithBlock:^{ - ASDisplayNodeAssertMainThread(); - LOG(@"Edit Command - reloadRows: %@", indexPaths); - - [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - - NSMutableArray *contexts = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - - // Sort indexPath to avoid messing up the index when deleting - // FIXME: Shouldn't deletes be sorted in descending order? - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - - id environment = [self.environmentDelegate dataControllerEnvironment]; - ASEnvironmentTraitCollection environmentTraitCollection = environment.environmentTraitCollection; - - for (NSIndexPath *indexPath in sortedIndexPaths) { - ASCellNodeBlock nodeBlock = [_dataSource dataController:self nodeBlockAtIndexPath:indexPath]; - ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:ASDataControllerRowNodeKind atIndexPath:indexPath]; - [contexts addObject:[[ASIndexedNodeContext alloc] initWithNodeBlock:nodeBlock - indexPath:indexPath - constrainedSize:constrainedSize - environmentTraitCollection:environmentTraitCollection]]; - } - - [self prepareForReloadRowsAtIndexPaths:indexPaths]; - - [_editingTransactionQueue addOperationWithBlock:^{ - [self willReloadRowsAtIndexPaths:indexPaths]; - - LOG(@"Edit Transaction - reloadRows: %@", indexPaths); - [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; - [self _batchLayoutNodesFromContexts:contexts withAnimationOptions:animationOptions]; - }]; - }]; + ASDisplayNodeAssert(NO, @"ASDataController does not support %@. Call this on ASChangeSetDataController and the reload will be broken into delete & insert.", NSStringFromSelector(_cmd)); } - (void)relayoutAllNodes diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h index fc8fc6cafe..54e9ac0126 100644 --- a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h @@ -10,7 +10,7 @@ @interface NSIndexSet (ASHelpers) -- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger))block; +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger idx))block; - (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes; @@ -20,4 +20,6 @@ /// If you've got an old index, and you insert items using this index set, this returns the new index. - (NSUInteger)as_indexByInsertingItemsBelowIndex:(NSUInteger)index; +- (NSString *)as_smallDescription; + @end diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m index 183ae959e4..feac7ec8d0 100644 --- a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m @@ -59,4 +59,18 @@ return newIndex; } +- (NSString *)as_smallDescription +{ + NSMutableString *result = [NSMutableString stringWithString:@"{ "]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + if (range.length == 1) { + [result appendFormat:@"%lu ", (unsigned long)range.location]; + } else { + [result appendFormat:@"%lu-%lu ", (unsigned long)range.location, (unsigned long)NSMaxRange(range)]; + } + }]; + [result appendString:@"}"]; + return result; +} + @end diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h index d46d0a3622..9387a91d56 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -23,6 +23,8 @@ typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { _ASHierarchyChangeTypeInsert }; +NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); + @interface _ASHierarchySectionChange : NSObject // FIXME: Generalize this to `changeMetadata` dict? @@ -49,8 +51,6 @@ typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { @property (nonatomic, strong, readonly) NSIndexSet *deletedSections; /// @precondition The change set must be completed. @property (nonatomic, strong, readonly) NSIndexSet *insertedSections; -/// @precondition The change set must be completed. -@property (nonatomic, strong, readonly) NSIndexSet *reloadedSections; /** Get the section index after the update for the given section before the update. diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m index 9b32376faf..26adef5cd1 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -13,6 +13,21 @@ #import "_ASHierarchyChangeSet.h" #import "ASInternalHelpers.h" #import "NSIndexSet+ASHelpers.h" +#import "ASAssert.h" + +NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) +{ + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + return @"Insert"; + case _ASHierarchyChangeTypeDelete: + return @"Delete"; + case _ASHierarchyChangeTypeReload: + return @"Reload"; + default: + return @"(invalid)"; + } +} @interface _ASHierarchySectionChange () - (instancetype)initWithChangeType:(_ASHierarchyChangeType)changeType indexSet:(NSIndexSet *)indexSet animationOptions:(ASDataControllerAnimationOptions)animationOptions; @@ -37,21 +52,6 @@ + (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections; @end -@implementation NSIndexSet (ASHierarchyHelpers) - -- (NSIndexSet *)intersectionWithIndexes:(NSIndexSet *)indexes -{ - NSMutableIndexSet *result = [NSMutableIndexSet indexSet]; - [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { - [indexes enumerateRangesInRange:range options:kNilOptions usingBlock:^(NSRange range, BOOL * _Nonnull stop) { - [result addIndexesInRange:range]; - }]; - }]; - return result; -} - -@end - @interface _ASHierarchyChangeSet () @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges; @@ -224,37 +224,30 @@ - (void)_sortAndCoalesceChangeArrays { @autoreleasepool { + + // Split reloaded section indexes into deletes and inserts + // Delete the old section, insert the new section it corresponds to. + for (_ASHierarchySectionChange *change in _reloadSectionChanges) { + NSIndexSet *newSections = [change.indexSet as_indexesByMapping:^(NSUInteger idx) { + NSUInteger newSec = [self newSectionForOldSection:idx]; + NSAssert(newSec != NSNotFound, nil); + return newSec; + }]; + + _ASHierarchySectionChange *deleteChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:change.indexSet animationOptions:change.animationOptions]; + [_deleteSectionChanges addObject:deleteChange]; + + _ASHierarchySectionChange *insertChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:newSections animationOptions:change.animationOptions]; + [_insertSectionChanges addObject:insertChange]; + } + + _reloadSectionChanges = nil; + [_ASHierarchySectionChange sortAndCoalesceChanges:_deleteSectionChanges]; [_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges]; [_ASHierarchySectionChange sortAndCoalesceChanges:_reloadSectionChanges]; - - _deletedSections = [[_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges] copy]; - _insertedSections = [[_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges] copy]; - _reloadedSections = [[_ASHierarchySectionChange allIndexesInSectionChanges:_reloadSectionChanges] copy]; - - NSIndexSet *deletedAndReloaded = [_deletedSections intersectionWithIndexes:_reloadedSections]; - NSAssert(deletedAndReloaded.count == 0, @"Request to delete and reload the same section(s): %@", deletedAndReloaded); - - // These are invalid old section indexes. - NSMutableIndexSet *deletedOrReloaded = [_deletedSections mutableCopy]; - [deletedOrReloaded addIndexes:_reloadedSections]; - - // These are invalid new section indexes. - NSMutableIndexSet *insertedOrReloaded = [_insertedSections mutableCopy]; - - // Get the new section that each reloaded section index corresponds to. - // Coalesce reload sections' indexes into deletes and inserts - [_reloadedSections enumerateIndexesUsingBlock:^(NSUInteger oldIndex, __unused BOOL * stop) { - NSUInteger newIndex = [self newSectionForOldSection:oldIndex]; - if (newIndex != NSNotFound) { - [insertedOrReloaded addIndex:newIndex]; - } - [deletedOrReloaded addIndex:oldIndex]; - }]; - - _deletedSections = deletedOrReloaded; - _insertedSections = insertedOrReloaded; - _reloadedSections = nil; + _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; + _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; // reload items changes need to be adjusted so that we access the correct indexPaths in the datasource NSDictionary *insertedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_insertItemChanges ofType:_ASHierarchyChangeTypeInsert]; @@ -296,13 +289,17 @@ [_reloadItemChanges removeAllObjects]; // Ignore item deletes in reloaded/deleted sections. - [_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:deletedOrReloaded]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; // Ignore item inserts in reloaded(new)/inserted sections. - [_ASHierarchyItemChange sortAndCoalesceChanges:_insertItemChanges ignoringChangesInSections:insertedOrReloaded]; + [_ASHierarchyItemChange sortAndCoalesceChanges:_insertItemChanges ignoringChangesInSections:_insertedSections]; } } +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p: deletedSections=%@, insertedSections=%@, deletedItems=%@, insertedItems=%@>", NSStringFromClass(self.class), self, _deletedSections, _insertedSections, _deleteItemChanges, _insertItemChanges]; +} @end @@ -313,6 +310,7 @@ { self = [super init]; if (self) { + ASDisplayNodeAssert(indexSet.count > 0, @"Request to create _ASHierarchySectionChange with no sections!"); _changeType = changeType; _indexSet = indexSet; _animationOptions = animationOptions; @@ -385,6 +383,11 @@ return indexes; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: anim=%lu, type=%@, indexes=%@>", NSStringFromClass(self.class), (unsigned long)_animationOptions, NSStringFromASHierarchyChangeType(_changeType), [self.indexSet as_smallDescription]]; +} + @end @implementation _ASHierarchyItemChange @@ -393,6 +396,7 @@ { self = [super init]; if (self) { + ASDisplayNodeAssert(indexPaths.count > 0, @"Request to create _ASHierarchyItemChange with no items!"); _changeType = changeType; if (presorted) { _indexPaths = indexPaths; @@ -489,4 +493,9 @@ [changes setArray:result]; } +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@: anim=%lu, type=%@, indexPaths=%@>", NSStringFromClass(self.class), (unsigned long)_animationOptions, NSStringFromASHierarchyChangeType(_changeType), self.indexPaths]; +} + @end diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index ffdfa7eec8..02910a1d06 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -446,17 +446,22 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; @implementation ASTableViewThrashTests { // The current update, which will be logged in case of a failure. ASThrashUpdate *_update; + BOOL _failed; } #pragma mark Overrides - (void)tearDown { + if (_failed && _update != nil) { + NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); + } + _failed = NO; _update = nil; } // NOTE: Despite the documentation, this is not always called if an exception is caught. - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)filePath atLine:(NSUInteger)lineNumber expected:(BOOL)expected { - [self logCurrentUpdateIfNeeded]; + _failed = YES; [super recordFailureWithDescription:description inFile:filePath atLine:lineNumber expected:expected]; } @@ -497,12 +502,6 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; #pragma mark Helpers -- (void)logCurrentUpdateIfNeeded { - if (_update != nil) { - NSLog(@"Failed update %@: %@", _update, _update.logFriendlyBase64Representation); - } -} - - (void)applyUpdate:(ASThrashUpdate *)update toDataSource:(ASThrashDataSource *)dataSource { TableView *tableView = dataSource.tableView; @@ -535,7 +534,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; [tableView waitUntilAllUpdatesAreCommitted]; #endif } @catch (NSException *exception) { - [self logCurrentUpdateIfNeeded]; + _failed = YES; @throw exception; } } From 9c70cec8d88a0436f17cad7927e1d06682da4dcf Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 16:58:23 -0700 Subject: [PATCH 03/12] Improve update handling more --- AsyncDisplayKit/ASCollectionView.mm | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 68031f464a..63bbefc603 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -498,18 +498,21 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)insertSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } [_dataController insertSections:sections withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)deleteSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } [_dataController deleteSections:sections withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)reloadSections:(NSIndexSet *)sections { ASDisplayNodeAssertMainThread(); + if (sections.count == 0) { return; } [_dataController reloadSections:sections withAnimationOptions:kASCollectionViewAnimationNone]; } @@ -522,18 +525,21 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } [_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } [_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); + if (indexPaths.count == 0) { return; } [_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone]; } From 681876bd59673872a97c185a8f21058cdf416135 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 17:10:33 -0700 Subject: [PATCH 04/12] Fix more issues with data integrity --- AsyncDisplayKit/Private/_ASHierarchyChangeSet.m | 7 ++++++- AsyncDisplayKitTests/ASTableViewThrashTests.m | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m index 26adef5cd1..0b2137fe75 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -131,6 +131,8 @@ 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; @@ -225,6 +227,10 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) { @autoreleasepool { + // Give these their "pre-reloads" values. Once we add in the reloads we'll re-process them. + _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; + _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; + // Split reloaded section indexes into deletes and inserts // Delete the old section, insert the new section it corresponds to. for (_ASHierarchySectionChange *change in _reloadSectionChanges) { @@ -245,7 +251,6 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) [_ASHierarchySectionChange sortAndCoalesceChanges:_deleteSectionChanges]; [_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges]; - [_ASHierarchySectionChange sortAndCoalesceChanges:_reloadSectionChanges]; _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 02910a1d06..92b3cc2f2c 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -146,7 +146,7 @@ static volatile int32_t ASThrashTestSectionNextID = 1; } - (NSString *)description { - return [NSString stringWithFormat:@"
", (unsigned long)_sectionID, (unsigned long)self.items.count]; + return [NSString stringWithFormat:@"
", (unsigned long)_sectionID, (unsigned long)self.items.count, ASThrashArrayDescription(self.items)]; } - (id)copyWithZone:(NSZone *)zone { @@ -554,7 +554,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; XCTAssertEqual([tableView rectForRowAtIndexPath:indexPath].size.height, item.rowHeight); #else ASThrashTestNode *node = (ASThrashTestNode *)[tableView nodeForRowAtIndexPath:indexPath]; - XCTAssertEqual(node.item, item); + XCTAssertEqualObjects(node.item, item, @"Wrong node at index path %@", indexPath); #endif } } From d8d2524b89b07bb683dd0b968136b28f611550b7 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 17:28:22 -0700 Subject: [PATCH 05/12] One more critical update integrity fix --- AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h | 4 ++-- AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m | 4 ++-- AsyncDisplayKit/Private/_ASHierarchyChangeSet.m | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h index 54e9ac0126..179e685639 100644 --- a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.h @@ -17,8 +17,8 @@ /// Returns all the item indexes from the given index paths that are in the given section. + (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section; -/// If you've got an old index, and you insert items using this index set, this returns the new index. -- (NSUInteger)as_indexByInsertingItemsBelowIndex:(NSUInteger)index; +/// If you've got an old index, and you insert items using this index set, this returns the change to get to the new index. +- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index; - (NSString *)as_smallDescription; diff --git a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m index feac7ec8d0..0837ad0eaa 100644 --- a/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m +++ b/AsyncDisplayKit/Details/NSIndexSet+ASHelpers.m @@ -46,7 +46,7 @@ return result; } -- (NSUInteger)as_indexByInsertingItemsBelowIndex:(NSUInteger)index +- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index { __block NSUInteger newIndex = index; [self enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) { @@ -56,7 +56,7 @@ *stop = YES; } }]; - return newIndex; + return newIndex - index; } - (NSString *)as_smallDescription diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m index 0b2137fe75..e7218f6d23 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -139,7 +139,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) } NSUInteger newIndex = oldSection - [_deletedSections countOfIndexesInRange:NSMakeRange(0, oldSection)]; - newIndex = [_insertedSections as_indexByInsertingItemsBelowIndex:newIndex]; + newIndex += [_insertedSections as_indexChangeByInsertingItemsBelowIndex:newIndex]; return newIndex; } @@ -276,7 +276,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) 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_indexByInsertingItemsBelowIndex:item]; + item += [indicesInsertedInSection as_indexChangeByInsertingItemsBelowIndex:item]; //TODO: reuse the old indexPath object if section and row aren't changed NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:item inSection:section]; From 5d72f2f2ccefc2d2aec9a0e2f08fe189aad0e6c2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 23 Jun 2016 17:30:18 -0700 Subject: [PATCH 06/12] Enable the thrash testing --- AsyncDisplayKitTests/ASTableViewThrashTests.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AsyncDisplayKitTests/ASTableViewThrashTests.m b/AsyncDisplayKitTests/ASTableViewThrashTests.m index 92b3cc2f2c..62ddffdd90 100644 --- a/AsyncDisplayKitTests/ASTableViewThrashTests.m +++ b/AsyncDisplayKitTests/ASTableViewThrashTests.m @@ -20,8 +20,8 @@ #define TableView ASTableView #endif -#define kInitialSectionCount 20 -#define kInitialItemCount 20 +#define kInitialSectionCount 10 +#define kInitialItemCount 10 #define kMinimumItemCount 5 #define kMinimumSectionCount 3 #define kFickleness 0.1 @@ -488,7 +488,7 @@ static NSInteger ASThrashUpdateCurrentSerializationVersion = 1; [self verifyDataSource:ds]; } -- (void)DISABLED_testThrashingWildly { +- (void)testThrashingWildly { for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; ASThrashDataSource *ds = [[ASThrashDataSource alloc] initWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; From 84e8b2686c84edd98f53f4d15fe6b819bb840fc9 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Jun 2016 13:03:51 -0700 Subject: [PATCH 07/12] [ASDataController] Combine isolated reloads into a batch --- AsyncDisplayKit/Details/ASChangeSetDataController.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.m index 7802a6c1d5..f6615d1bdb 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.m @@ -109,8 +109,10 @@ if ([self batchUpdating]) { [_changeSet reloadSections:sections animationOptions:animationOptions]; } else { + [self beginUpdates]; [super deleteSections:sections withAnimationOptions:animationOptions]; [super insertSections:sections withAnimationOptions:animationOptions]; + [self endUpdates]; } } @@ -153,8 +155,10 @@ if ([self batchUpdating]) { [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; } else { + [self beginUpdates]; [super deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [super insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self endUpdates]; } } From 304f8f6cb1e06bcb9b720697d66d87792c825551 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Jun 2016 13:17:00 -0700 Subject: [PATCH 08/12] [ASHierarchyChangeSet] Clean up and add documentation --- .../Private/_ASHierarchyChangeSet.h | 4 ++-- .../Private/_ASHierarchyChangeSet.m | 21 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h index 9387a91d56..f514be705b 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -87,8 +87,8 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); - Inserted sections, ascending order - Inserted items, ascending order */ -- (NSArray <_ASHierarchySectionChange *> *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; -- (NSArray <_ASHierarchyItemChange *> *)itemChangesOfType:(_ASHierarchyChangeType)changeType; +- (nullable NSArray <_ASHierarchySectionChange *> *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; +- (nullable NSArray <_ASHierarchyItemChange *> *)itemChangesOfType:(_ASHierarchyChangeType)changeType; /// Returns all item indexes affected by changes of the given type in the given section. - (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section; diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m index e7218f6d23..9075f36878 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -227,16 +227,16 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) { @autoreleasepool { + // Split reloaded sections into [delete(oldIndex), insert(newIndex)] + // Give these their "pre-reloads" values. Once we add in the reloads we'll re-process them. _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; - // Split reloaded section indexes into deletes and inserts - // Delete the old section, insert the new section it corresponds to. for (_ASHierarchySectionChange *change in _reloadSectionChanges) { NSIndexSet *newSections = [change.indexSet as_indexesByMapping:^(NSUInteger idx) { NSUInteger newSec = [self newSectionForOldSection:idx]; - NSAssert(newSec != NSNotFound, nil); + NSAssert(newSec != NSNotFound, @"Request to reload deleted section %lu", (unsigned long)idx); return newSec; }]; @@ -254,13 +254,14 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; - // reload items changes need to be adjusted so that we access the correct indexPaths in the datasource + // Split reloaded items into [delete(oldIndexPath), insert(newIndexPath)] + NSDictionary *insertedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_insertItemChanges ofType:_ASHierarchyChangeTypeInsert]; NSDictionary *deletedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_deleteItemChanges ofType:_ASHierarchyChangeTypeDelete]; for (_ASHierarchyItemChange *change in _reloadItemChanges) { NSAssert(change.changeType == _ASHierarchyChangeTypeReload, @"It must be a reload change to be in here"); - NSMutableArray *newIndexPaths = [NSMutableArray array]; + 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 @@ -278,7 +279,6 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) NSIndexSet *indicesInsertedInSection = insertedIndexPathsMap[@(section)]; item += [indicesInsertedInSection as_indexChangeByInsertingItemsBelowIndex:item]; - //TODO: reuse the old indexPath object if section and row aren't changed NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:item inSection:section]; [newIndexPaths addObject:newIndexPath]; } @@ -291,7 +291,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) _ASHierarchyItemChange *insertItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:newIndexPaths animationOptions:change.animationOptions presorted:NO]; [_insertItemChanges addObject:insertItemChangeFromReloadChange]; } - [_reloadItemChanges removeAllObjects]; + _reloadItemChanges = nil; // Ignore item deletes in reloaded/deleted sections. [_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; @@ -436,7 +436,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) return sectionToIndexSetMap; } -+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections ++ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)ignoredSections { if (changes.count < 1) { return; @@ -450,12 +450,9 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) // All changed index paths, sorted NSMutableArray *allIndexPaths = [NSMutableArray new]; - NSPredicate *indexPathInValidSection = [NSPredicate predicateWithBlock:^BOOL(NSIndexPath *indexPath, __unused NSDictionary *_) { - return ![sections containsIndex:indexPath.section]; - }]; for (_ASHierarchyItemChange *change in changes) { for (NSIndexPath *indexPath in change.indexPaths) { - if ([indexPathInValidSection evaluateWithObject:indexPath]) { + if (![ignoredSections containsIndex:indexPath.section]) { animationOptions[indexPath] = @(change.animationOptions); [allIndexPaths addObject:indexPath]; } From 27dc52c0c506e689d988373e8379bccd294cf38c Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Jun 2016 13:20:06 -0700 Subject: [PATCH 09/12] [ASHierarchyChangeSet] Document reload-splitting behavior --- AsyncDisplayKit/Private/_ASHierarchyChangeSet.h | 1 + 1 file changed, 1 insertion(+) diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h index f514be705b..782607fd0c 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -71,6 +71,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); @property (nonatomic, readonly) BOOL completed; /// 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)markCompleted; /** From 0a525a3c16f3dca6245f2b9be4677f85f56ff8d7 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Jun 2016 13:52:16 -0700 Subject: [PATCH 10/12] [ASFlowLayoutController] Fix enumeration bug in ASFlowLayoutController --- AsyncDisplayKit/Details/ASFlowLayoutController.mm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm index cb754bb9bf..ffd553ba96 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.mm +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -92,12 +92,12 @@ currPath.row++; // Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. - while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < completedNodes.count - 1) { + while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < endPath.section) { currPath.row = 0; currPath.section++; - ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath"); } } + ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath"); [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]]; From 211dcdf0e8f827347c3fceecd019067728b8ad6d Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Jun 2016 13:56:08 -0700 Subject: [PATCH 11/12] [_ASHierarchyChangeSet] Remove unused new method --- .../Private/_ASHierarchyChangeSet.h | 8 ------- .../Private/_ASHierarchyChangeSet.m | 24 ------------------- 2 files changed, 32 deletions(-) diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h index 782607fd0c..755d82004d 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -60,14 +60,6 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); */ - (NSUInteger)newSectionForOldSection:(NSUInteger)oldSection; -/** - Get the index path after the update for the item at the given index path before the update. - - @precondition The change set must be completed. - @returns The new index path, or nil if the given item (or its section) was deleted. - */ -- (nullable NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath; - @property (nonatomic, readonly) BOOL completed; /// 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. diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m index 9075f36878..d1b54438a0 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.m @@ -143,30 +143,6 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) return newIndex; } -- (nullable NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath -{ - [self _ensureCompleted]; - // If section was deleted, nil - NSUInteger oldSection = indexPath.section; - NSUInteger newSection = [self newSectionForOldSection:oldSection]; - if (newSection == NSNotFound) { - return nil; - } - - NSUInteger newItem = indexPath.item; - NSIndexSet *deletedItemsInOldSection = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeDelete inSection:oldSection]; - newItem -= [deletedItemsInOldSection countOfIndexesInRange:NSMakeRange(0, newItem)]; - - for (_ASHierarchyItemChange *change in _deleteItemChanges) { - // If item was deleted, nil - if ([change.indexPaths containsObject:indexPath]) { - return nil; - } - } - - return [NSIndexPath indexPathForItem:newItem inSection:newSection]; -} - - (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options { [self _ensureNotCompleted]; From 0a354f8f4eaf6ddfb44b891a4d8f102453c27ed2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 24 Jun 2016 15:48:23 -0700 Subject: [PATCH 12/12] [ASDataController] Remove implementation for unused hooks --- AsyncDisplayKit/Details/ASDataController.mm | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 74869de0d7..163b812e10 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -725,16 +725,6 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; // Optional template hook for subclasses (See ASDataController+Subclasses.h) } -- (void)prepareForReloadSections:(NSIndexSet *)sections -{ - // Optional template hook for subclasses (See ASDataController+Subclasses.h) -} - -- (void)willReloadSections:(NSIndexSet *)sections -{ - // Optional template hook for subclasses (See ASDataController+Subclasses.h) -} - - (void)willMoveSection:(NSInteger)section toSection:(NSInteger)newSection { // Optional template hook for subclasses (See ASDataController+Subclasses.h)