diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a2f6965f13..9be9cc6860 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -273,13 +273,13 @@ 9CFFC6BE1CCAC52B006A6476 /* ASEnvironment.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFFC6BD1CCAC52B006A6476 /* ASEnvironment.mm */; }; 9CFFC6C01CCAC73C006A6476 /* ASViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */; }; 9CFFC6C21CCAC768006A6476 /* ASTableNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */; }; - 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */; }; + 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */; }; A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */; }; A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = A373200E1C571B050011FC94 /* ASTextNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; AC026B581BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */; }; AC026B6A1BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */; settings = {ATTRIBUTES = (Public, ); }; }; - AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */; }; - AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */; }; + AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */; }; + AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */; }; AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */ = {isa = PBXBuildFile; fileRef = AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */; }; AC026B711BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */; }; AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */ = {isa = PBXBuildFile; fileRef = AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */; }; @@ -1011,14 +1011,14 @@ 9CFFC6BD1CCAC52B006A6476 /* ASEnvironment.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASEnvironment.mm; sourceTree = ""; }; 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASViewController.mm; sourceTree = ""; }; 9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableNode.mm; sourceTree = ""; }; - 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionViewTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; + 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionViewTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASPINRemoteImageDownloader.h; path = Details/ASPINRemoteImageDownloader.h; sourceTree = ""; }; A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASPINRemoteImageDownloader.m; path = Details/ASPINRemoteImageDownloader.m; sourceTree = ""; }; A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitFontSizeAdjuster.h; path = TextKit/ASTextKitFontSizeAdjuster.h; sourceTree = ""; }; A373200E1C571B050011FC94 /* ASTextNode+Beta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASTextNode+Beta.h"; sourceTree = ""; }; AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASStaticLayoutSpecSnapshotTests.m; sourceTree = ""; }; AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASChangeSetDataController.h; sourceTree = ""; }; - AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASChangeSetDataController.m; sourceTree = ""; }; + AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASChangeSetDataController.mm; sourceTree = ""; }; AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASHierarchyChangeSet.h; sourceTree = ""; }; AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASHierarchyChangeSet.mm; sourceTree = ""; }; AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutDefines.h; path = AsyncDisplayKit/Layout/ASStackLayoutDefines.h; sourceTree = ""; }; @@ -1346,7 +1346,7 @@ 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m */, CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */, 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.m */, - 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */, + 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */, 2911485B1A77147A005D0878 /* ASControlNodeTests.m */, ACF6ED541B178DC700DA7C62 /* ASDimensionTests.mm */, 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.m */, @@ -1582,7 +1582,7 @@ 464052191A3F83C40061C0BA /* ASDataController.h */, 4640521A1A3F83C40061C0BA /* ASDataController.mm */, AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */, - AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */, + AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */, E5711A2A1C840C81009619D4 /* ASIndexedNodeContext.h */, E5711A2D1C840C96009619D4 /* ASIndexedNodeContext.mm */, ); @@ -2145,7 +2145,7 @@ 9C70F2041CDA4EFA007D6C76 /* ASTraitCollection.m in Sources */, 92074A691CC8BADA00918F75 /* ASControlNode+tvOS.m in Sources */, ACF6ED321B17843500DA7C62 /* ASStaticLayoutSpec.mm in Sources */, - AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, + AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */, 68355B311CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */, 9CFFC6C01CCAC73C006A6476 /* ASViewController.mm in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, @@ -2169,7 +2169,7 @@ 242995D31B29743C00090100 /* ASBasicImageDownloaderTests.m in Sources */, 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.m in Sources */, ACF6ED5C1B178DC700DA7C62 /* ASCenterLayoutSpecSnapshotTests.mm in Sources */, - 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.m in Sources */, + 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */, 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */, CC3B208E1C3F7D0A00798563 /* ASWeakSetTests.m in Sources */, F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */, @@ -2311,7 +2311,7 @@ 34EFC7781B701D3100AD841F /* ASStackUnpositionedLayout.mm in Sources */, DE84918E1C8FFF9F003D89E9 /* ASRunLoopQueue.mm in Sources */, 68FC85E51CE29B7E00EDD713 /* ASTabBarController.m in Sources */, - AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, + AC026B6C1BD57D6F00BBC17E /* ASChangeSetDataController.mm in Sources */, 34EFC7741B701D0A00AD841F /* ASStaticLayoutSpec.mm in Sources */, 92074A6A1CC8BADA00918F75 /* ASControlNode+tvOS.m in Sources */, DB78412E1C6BCE1600A9E2B4 /* _ASTransitionContext.m in Sources */, diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index eb76bb470c..b44715c97e 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -23,6 +23,20 @@ ASDISPLAYNODE_EXTERN_C_END + (BOOL)usesImplicitHierarchyManagement; + (void)setUsesImplicitHierarchyManagement:(BOOL)enabled; +/** + * ASTableView and ASCollectionView now throw exceptions on invalid updates + * like their UIKit counterparts. If YES, these classes will log messages + * on invalid updates rather than throwing exceptions. + * + * Note that even if AsyncDisplayKit's exception is suppressed, the app may still crash + * as it proceeds with an invalid update. + * + * This currently defaults to YES. In a future release it will default to NO and later + * be removed entirely. + */ ++ (BOOL)suppressesInvalidCollectionUpdateExceptions; ++ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses; + /** @name Layout */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 3eb2b26ee1..4dac0e4e5e 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -89,6 +89,18 @@ static BOOL usesImplicitHierarchyManagement = NO; usesImplicitHierarchyManagement = enabled; } +static BOOL suppressesInvalidCollectionUpdateExceptions = YES; + ++ (BOOL)suppressesInvalidCollectionUpdateExceptions +{ + return suppressesInvalidCollectionUpdateExceptions; +} + ++ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses +{ + suppressesInvalidCollectionUpdateExceptions = suppresses; +} + BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) { return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); diff --git a/AsyncDisplayKit/Details/ASChangeSetDataController.m b/AsyncDisplayKit/Details/ASChangeSetDataController.mm similarity index 62% rename from AsyncDisplayKit/Details/ASChangeSetDataController.m rename to AsyncDisplayKit/Details/ASChangeSetDataController.mm index 99a5bc81b2..0660b87d49 100644 --- a/AsyncDisplayKit/Details/ASChangeSetDataController.m +++ b/AsyncDisplayKit/Details/ASChangeSetDataController.mm @@ -13,6 +13,7 @@ #import "ASChangeSetDataController.h" #import "_ASHierarchyChangeSet.h" #import "ASAssert.h" +#import "ASDataController+Subclasses.h" @implementation ASChangeSetDataController { NSInteger _changeSetBatchUpdateCounter; @@ -26,8 +27,8 @@ // NOTE: This assertion is failing in some apps and will be enabled soon. // ASDisplayNodeAssertMainThread(); if (_changeSetBatchUpdateCounter <= 0) { - _changeSet = [_ASHierarchyChangeSet new]; _changeSetBatchUpdateCounter = 0; + _changeSet = [[_ASHierarchyChangeSet alloc] initWithOldData:[self itemCountsFromDataSource]]; } _changeSetBatchUpdateCounter++; } @@ -43,12 +44,18 @@ // NSAssert(_changeSetBatchUpdateCounter >= 0, @"endUpdatesAnimated:completion: called without having a balanced beginUpdates call"); if (_changeSetBatchUpdateCounter == 0) { - [_changeSet markCompleted]; + if (!self.initialReloadDataHasBeenCalled) { + if (completion) { + completion(YES); + } + _changeSet = nil; + return; + } + + [self invalidateDataSourceItemCounts]; + [_changeSet markCompletedWithNewItemCounts:[self itemCountsFromDataSource]]; [super beginUpdates]; - - 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]; @@ -85,45 +92,34 @@ - (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet insertSections:sections animationOptions:animationOptions]; - } else { - [super insertSections:sections withAnimationOptions:animationOptions]; - } + [self beginUpdates]; + [_changeSet insertSections:sections animationOptions:animationOptions]; + [self endUpdates]; } - (void)deleteSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet deleteSections:sections animationOptions:animationOptions]; - } else { - [super deleteSections:sections withAnimationOptions:animationOptions]; - } + [self beginUpdates]; + [_changeSet deleteSections:sections animationOptions:animationOptions]; + [self endUpdates]; } - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet reloadSections:sections animationOptions:animationOptions]; - } else { - [self beginUpdates]; - [super deleteSections:sections withAnimationOptions:animationOptions]; - [super insertSections:sections withAnimationOptions:animationOptions]; - [self endUpdates]; - } + [self beginUpdates]; + [_changeSet reloadSections:sections animationOptions:animationOptions]; + [self endUpdates]; } - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; - [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; - } else { - [super moveSection:section toSection:newSection withAnimationOptions:animationOptions]; - } + [self beginUpdates]; + [_changeSet deleteSections:[NSIndexSet indexSetWithIndex:section] animationOptions:animationOptions]; + [_changeSet insertSections:[NSIndexSet indexSetWithIndex:newSection] animationOptions:animationOptions]; + [self endUpdates]; } #pragma mark - Row Editing (External API) @@ -131,45 +127,34 @@ - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet insertItems:indexPaths animationOptions:animationOptions]; - } else { - [super insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - } + [self beginUpdates]; + [_changeSet insertItems:indexPaths animationOptions:animationOptions]; + [self endUpdates]; } - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet deleteItems:indexPaths animationOptions:animationOptions]; - } else { - [super deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - } + [self beginUpdates]; + [_changeSet deleteItems:indexPaths animationOptions:animationOptions]; + [self endUpdates]; } - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; - } else { - [self beginUpdates]; - [super deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [super insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - [self endUpdates]; - } + [self beginUpdates]; + [_changeSet reloadItems:indexPaths animationOptions:animationOptions]; + [self endUpdates]; } - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { ASDisplayNodeAssertMainThread(); - if ([self batchUpdating]) { - [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; - [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; - } else { - [super moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:animationOptions]; - } + [self beginUpdates]; + [_changeSet deleteItems:@[indexPath] animationOptions:animationOptions]; + [_changeSet insertItems:@[newIndexPath] animationOptions:animationOptions]; + [self endUpdates]; } @end diff --git a/AsyncDisplayKit/Details/ASDataController+Subclasses.h b/AsyncDisplayKit/Details/ASDataController+Subclasses.h index 099a9bfe45..7df6e8b2d9 100644 --- a/AsyncDisplayKit/Details/ASDataController+Subclasses.h +++ b/AsyncDisplayKit/Details/ASDataController+Subclasses.h @@ -9,6 +9,7 @@ // #pragma once +#import @class ASIndexedNodeContext; @@ -33,6 +34,21 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray *nodes, NS */ - (NSMutableArray *)completedNodesOfKind:(NSString *)kind; +/** + * Ensure that next time `itemCountsFromDataSource` is called, new values are retrieved. + * + * This must be called on the main thread. + */ +- (void)invalidateDataSourceItemCounts; + +/** + * Returns the most recently gathered item counts from the data source. If the counts + * have been invalidated, this synchronously queries the data source and saves the result. + * + * This must be called on the main thread. + */ +- (std::vector)itemCountsFromDataSource; + #pragma mark - Node sizing /** diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index a740d4f1ce..84599c1a97 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -124,6 +124,15 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind; */ @property (nonatomic, weak) id environmentDelegate; +/** + * Returns YES if reloadData has been called at least once. Before this point it is + * important to ignore/suppress some operations. For example, inserting a section + * before the initial data load should have no effect. + * + * This must be called on the main thread. + */ +@property (nonatomic, readonly) BOOL initialReloadDataHasBeenCalled; + /** @name Data Updating */ - (void)beginUpdates; diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index 08bab41596..cd43b9e4a1 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -35,6 +35,8 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; NSMutableArray *_externalCompletedNodes; // Main thread only. External data access can immediately query this if available. NSMutableDictionary *_completedNodes; // Main thread only. External data access can immediately query this if _externalCompletedNodes is unavailable. NSMutableDictionary *_editingNodes; // Modified on _editingTransactionQueue only. Updates propagated to _completedNodes. + BOOL _itemCountsFromDataSourceAreValid; // Main thread only. + std::vector _itemCountsFromDataSource; // Main thread only. ASMainSerialQueue *_mainSerialQueue; @@ -237,6 +239,7 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; - (void)insertNodes:(NSArray *)nodes ofKind:(NSString *)kind atIndexPaths:(NSArray *)indexPaths completion:(ASDataControllerCompletionBlock)completionBlock { + ASSERT_ON_EDITING_QUEUE; if (!indexPaths.count || _dataSource == nil) { return; } @@ -411,6 +414,10 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; NSIndexSet *sectionIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; NSArray *contexts = [self _populateFromDataSourceWithSectionIndexSet:sectionIndexSet]; + [self invalidateDataSourceItemCounts]; + // Fetch the new item counts upfront. + [self itemCountsFromDataSource]; + // Allow subclasses to perform setup before going into the edit transaction [self prepareForReloadData]; @@ -494,6 +501,29 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; return contexts; } +- (void)invalidateDataSourceItemCounts +{ + ASDisplayNodeAssertMainThread(); + _itemCountsFromDataSourceAreValid = NO; +} + +- (std::vector)itemCountsFromDataSource +{ + ASDisplayNodeAssertMainThread(); + if (NO == _itemCountsFromDataSourceAreValid) { + id source = self.dataSource; + NSInteger sectionCount = [source numberOfSectionsInDataController:self]; + std::vector newCounts; + newCounts.reserve(sectionCount); + for (NSInteger i = 0; i < sectionCount; i++) { + newCounts.push_back([source dataController:self rowsInSection:i]); + } + _itemCountsFromDataSource = newCounts; + _itemCountsFromDataSourceAreValid = YES; + } + return _itemCountsFromDataSource; +} + #pragma mark - Batching (External API) - (void)beginUpdates @@ -720,7 +750,6 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; [self performEditCommandWithBlock:^{ ASDisplayNodeAssertMainThread(); LOG(@"Edit Command - insertRows: %@", indexPaths); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); // Sort indexPath to avoid messing up the index when inserting in several batches diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h index ebabc3e950..af5839508c 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.h @@ -11,17 +11,51 @@ // #import +#import NS_ASSUME_NONNULL_BEGIN typedef NSUInteger ASDataControllerAnimationOptions; typedef NS_ENUM(NSInteger, _ASHierarchyChangeType) { + /** + * A reload change, as submitted by the user. When a change set is + * completed, these changes are decomposed into delete-insert pairs + * and combined with the original deletes and inserts of the change. + */ _ASHierarchyChangeTypeReload, + + /** + * A change that was either an original delete, or the first + * part of a decomposed reload. + */ _ASHierarchyChangeTypeDelete, - _ASHierarchyChangeTypeInsert + + /** + * A change that was submitted by the user as a delete. + */ + _ASHierarchyChangeTypeOriginalDelete, + + /** + * A change that was either an original insert, or the second + * part of a decomposed reload. + */ + _ASHierarchyChangeTypeInsert, + + /** + * A change that was submitted by the user as an insert. + */ + _ASHierarchyChangeTypeOriginalInsert }; +/** + * Returns YES if the given change type is either .Insert or .Delete, NO otherwise. + * Other change types – .Reload, .OriginalInsert, .OriginalDelete – are + * intermediary types used while building the change set. All changes will + * be reduced to either .Insert or .Delete when the change is marked completed. + */ +BOOL ASHierarchyChangeTypeIsFinal(_ASHierarchyChangeType changeType); + NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); @interface _ASHierarchySectionChange : NSObject @@ -31,6 +65,12 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); @property (nonatomic, strong, readonly) NSIndexSet *indexSet; @property (nonatomic, readonly) _ASHierarchyChangeType changeType; + +/** + * If this is a .OriginalInsert or .OriginalDelete change, this returns a copied change + * with type .Insert or .Delete. Calling this on changes of other types is an error. + */ +- (_ASHierarchySectionChange *)changeByFinalizingType; @end @interface _ASHierarchyItemChange : NSObject @@ -42,10 +82,18 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); @property (nonatomic, readonly) _ASHierarchyChangeType changeType; + (NSDictionary *)sectionToIndexSetMapFromChanges:(NSArray<_ASHierarchyItemChange *> *)changes ofType:(_ASHierarchyChangeType)changeType; + +/** + * If this is a .OriginalInsert or .OriginalDelete change, this returns a copied change + * with type .Insert or .Delete. Calling this on changes of other types is an error. + */ +- (_ASHierarchyItemChange *)changeByFinalizingType; @end @interface _ASHierarchyChangeSet : NSObject +- (instancetype)initWithOldData:(std::vector)oldItemCounts NS_DESIGNATED_INITIALIZER; + /// @precondition The change set must be completed. @property (nonatomic, strong, readonly) NSIndexSet *deletedSections; /// @precondition The change set must be completed. @@ -63,22 +111,8 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); /// 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; +- (void)markCompletedWithNewItemCounts:(std::vector)newItemCounts; -/** - @abstract Return sorted changes of the given type, grouped by animation options. - - Items deleted from deleted sections are not reported. - Items inserted into inserted sections are not reported. - Items reloaded in reloaded sections are not reported. - - The safe order for processing change groups is: - - Reloaded sections & reloaded items - - Deleted items, descending order - - Deleted sections, descending order - - Inserted sections, ascending order - - Inserted items, ascending order - */ - (nullable NSArray <_ASHierarchySectionChange *> *)sectionChangesOfType:(_ASHierarchyChangeType)changeType; - (nullable NSArray <_ASHierarchyItemChange *> *)itemChangesOfType:(_ASHierarchyChangeType)changeType; diff --git a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm index 104c58ebb7..aaf6998e44 100644 --- a/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm +++ b/AsyncDisplayKit/Private/_ASHierarchyChangeSet.mm @@ -14,15 +14,37 @@ #import "ASInternalHelpers.h" #import "NSIndexSet+ASHelpers.h" #import "ASAssert.h" +#import "ASDisplayNode+Beta.h" #import +#define ASFailUpdateValidation(...)\ + if ([ASDisplayNode suppressesInvalidCollectionUpdateExceptions]) {\ + NSLog(__VA_ARGS__);\ + } else {\ + ASDisplayNodeFailAssert(__VA_ARGS__);\ + } + +BOOL ASHierarchyChangeTypeIsFinal(_ASHierarchyChangeType changeType) { + switch (changeType) { + case _ASHierarchyChangeTypeInsert: + case _ASHierarchyChangeTypeDelete: + return YES; + default: + return NO; + } +} + NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) { switch (changeType) { case _ASHierarchyChangeTypeInsert: return @"Insert"; + case _ASHierarchyChangeTypeOriginalInsert: + return @"OriginalInsert"; case _ASHierarchyChangeTypeDelete: return @"Delete"; + case _ASHierarchyChangeTypeOriginalDelete: + return @"OriginalDelete"; case _ASHierarchyChangeTypeReload: return @"Reload"; default: @@ -35,9 +57,9 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) /** On return `changes` is sorted according to the change type with changes coalesced by animationOptions - Assumes: `changes` is [_ASHierarchySectionChange] all with the same changeType + Assumes: `changes` all have the same changeType */ -+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes; ++ (void)sortAndCoalesceSectionChanges:(NSMutableArray<_ASHierarchySectionChange *> *)changes; /// Returns all the indexes from all the `indexSet`s of the given `_ASHierarchySectionChange` objects. + (NSMutableIndexSet *)allIndexesInSectionChanges:(NSArray *)changes; @@ -48,46 +70,72 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) /** On return `changes` is sorted according to the change type with changes coalesced by animationOptions - Assumes: `changes` is [_ASHierarchyItemChange] all with the same changeType + Assumes: `changes` all have the same changeType */ -+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)sections; ++ (void)sortAndCoalesceItemChanges:(NSMutableArray<_ASHierarchyItemChange *> *)changes ignoringChangesInSections:(NSIndexSet *)sections; @end @interface _ASHierarchyChangeSet () @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *insertItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *originalInsertItemChanges; + @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *deleteItemChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *originalDeleteItemChanges; + @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchyItemChange *> *reloadItemChanges; + @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *insertSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *originalInsertSectionChanges; + @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *deleteSectionChanges; +@property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *originalDeleteSectionChanges; + @property (nonatomic, strong, readonly) NSMutableArray<_ASHierarchySectionChange *> *reloadSectionChanges; @end -@implementation _ASHierarchyChangeSet +@implementation _ASHierarchyChangeSet { + std::vector _oldItemCounts; + std::vector _newItemCounts; +} - (instancetype)init +{ + ASFailUpdateValidation(@"_ASHierarchyChangeSet: -init is not supported. Call -initWithOldData:"); + return [self initWithOldData:std::vector()]; +} + +- (instancetype)initWithOldData:(std::vector)oldItemCounts { self = [super init]; if (self) { + _oldItemCounts = oldItemCounts; - _insertItemChanges = [NSMutableArray new]; - _deleteItemChanges = [NSMutableArray new]; - _reloadItemChanges = [NSMutableArray new]; - _insertSectionChanges = [NSMutableArray new]; - _deleteSectionChanges = [NSMutableArray new]; - _reloadSectionChanges = [NSMutableArray new]; + _originalInsertItemChanges = [[NSMutableArray alloc] init]; + _insertItemChanges = [[NSMutableArray alloc] init]; + _originalDeleteItemChanges = [[NSMutableArray alloc] init]; + _deleteItemChanges = [[NSMutableArray alloc] init]; + _reloadItemChanges = [[NSMutableArray alloc] init]; + + _originalInsertSectionChanges = [[NSMutableArray alloc] init]; + _insertSectionChanges = [[NSMutableArray alloc] init]; + _originalDeleteSectionChanges = [[NSMutableArray alloc] init]; + _deleteSectionChanges = [[NSMutableArray alloc] init]; + _reloadSectionChanges = [[NSMutableArray alloc] init]; } return self; } #pragma mark External API -- (void)markCompleted +- (void)markCompletedWithNewItemCounts:(std::vector)newItemCounts { NSAssert(!_completed, @"Attempt to mark already-completed changeset as completed."); _completed = YES; + _newItemCounts = newItemCounts; [self _sortAndCoalesceChangeArrays]; + [self _validateUpdate]; } - (NSArray *)sectionChangesOfType:(_ASHierarchyChangeType)changeType @@ -100,8 +148,13 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) return _reloadSectionChanges; case _ASHierarchyChangeTypeDelete: return _deleteSectionChanges; + case _ASHierarchyChangeTypeOriginalDelete: + return _originalDeleteSectionChanges; + case _ASHierarchyChangeTypeOriginalInsert: + return _originalInsertSectionChanges; default: NSAssert(NO, @"Request for section changes with invalid type: %lu", (long)changeType); + return nil; } } @@ -115,8 +168,13 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) return _reloadItemChanges; case _ASHierarchyChangeTypeDelete: return _deleteItemChanges; + case _ASHierarchyChangeTypeOriginalInsert: + return _originalInsertItemChanges; + case _ASHierarchyChangeTypeOriginalDelete: + return _originalDeleteItemChanges; default: NSAssert(NO, @"Request for item changes with invalid type: %lu", (long)changeType); + return nil; } } @@ -147,29 +205,29 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) - (void)deleteItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options { [self _ensureNotCompleted]; - _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexPaths:indexPaths animationOptions:options presorted:NO]; - [_deleteItemChanges addObject:change]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalDelete indexPaths:indexPaths animationOptions:options presorted:NO]; + [_originalDeleteItemChanges addObject:change]; } - (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options { [self _ensureNotCompleted]; - _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeDelete indexSet:sections animationOptions:options]; - [_deleteSectionChanges addObject:change]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalDelete indexSet:sections animationOptions:options]; + [_originalDeleteSectionChanges addObject:change]; } - (void)insertItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options { [self _ensureNotCompleted]; - _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:indexPaths animationOptions:options presorted:NO]; - [_insertItemChanges addObject:change]; + _ASHierarchyItemChange *change = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalInsert indexPaths:indexPaths animationOptions:options presorted:NO]; + [_originalInsertItemChanges addObject:change]; } - (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options { [self _ensureNotCompleted]; - _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:sections animationOptions:options]; - [_insertSectionChanges addObject:change]; + _ASHierarchySectionChange *change = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeOriginalInsert indexSet:sections animationOptions:options]; + [_originalInsertSectionChanges addObject:change]; } - (void)reloadItems:(NSArray *)indexPaths animationOptions:(ASDataControllerAnimationOptions)options @@ -207,13 +265,19 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) // 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]; + _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_originalDeleteSectionChanges]; + _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_originalInsertSectionChanges]; + for (_ASHierarchySectionChange *originalDeleteSectionChange in _originalDeleteSectionChanges) { + [_deleteSectionChanges addObject:[originalDeleteSectionChange changeByFinalizingType]]; + } + for (_ASHierarchySectionChange *originalInsertSectionChange in _originalInsertSectionChanges) { + [_insertSectionChanges addObject:[originalInsertSectionChange changeByFinalizingType]]; + } for (_ASHierarchySectionChange *change in _reloadSectionChanges) { NSIndexSet *newSections = [change.indexSet as_indexesByMapping:^(NSUInteger idx) { NSUInteger newSec = [self newSectionForOldSection:idx]; - NSAssert(newSec != NSNotFound, @"Request to reload deleted section %lu", (unsigned long)idx); + ASDisplayNodeAssert(newSec != NSNotFound, @"Request to reload and delete same section %zu", idx); return newSec; }]; @@ -223,15 +287,19 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) _ASHierarchySectionChange *insertChange = [[_ASHierarchySectionChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexSet:newSections animationOptions:change.animationOptions]; [_insertSectionChanges addObject:insertChange]; } - - _reloadSectionChanges = nil; - [_ASHierarchySectionChange sortAndCoalesceChanges:_deleteSectionChanges]; - [_ASHierarchySectionChange sortAndCoalesceChanges:_insertSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceSectionChanges:_deleteSectionChanges]; + [_ASHierarchySectionChange sortAndCoalesceSectionChanges:_insertSectionChanges]; _deletedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_deleteSectionChanges]; _insertedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_insertSectionChanges]; // Split reloaded items into [delete(oldIndexPath), insert(newIndexPath)] + for (_ASHierarchyItemChange *originalDeleteItemChange in _originalDeleteItemChanges) { + [_deleteItemChanges addObject:[originalDeleteItemChange changeByFinalizingType]]; + } + for (_ASHierarchyItemChange *originalInsertItemChange in _originalInsertItemChanges) { + [_insertItemChanges addObject:[originalInsertItemChange changeByFinalizingType]]; + } NSDictionary *insertedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_insertItemChanges ofType:_ASHierarchyChangeTypeInsert]; NSDictionary *deletedIndexPathsMap = [_ASHierarchyItemChange sectionToIndexSetMapFromChanges:_deleteItemChanges ofType:_ASHierarchyChangeTypeDelete]; @@ -268,13 +336,124 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) _ASHierarchyItemChange *insertItemChangeFromReloadChange = [[_ASHierarchyItemChange alloc] initWithChangeType:_ASHierarchyChangeTypeInsert indexPaths:newIndexPaths animationOptions:change.animationOptions presorted:NO]; [_insertItemChanges addObject:insertItemChangeFromReloadChange]; } - _reloadItemChanges = nil; // Ignore item deletes in reloaded/deleted sections. - [_ASHierarchyItemChange sortAndCoalesceChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; + [_ASHierarchyItemChange sortAndCoalesceItemChanges:_deleteItemChanges ignoringChangesInSections:_deletedSections]; // Ignore item inserts in reloaded(new)/inserted sections. - [_ASHierarchyItemChange sortAndCoalesceChanges:_insertItemChanges ignoringChangesInSections:_insertedSections]; + [_ASHierarchyItemChange sortAndCoalesceItemChanges:_insertItemChanges ignoringChangesInSections:_insertedSections]; + } +} + +- (void)_validateUpdate +{ + NSIndexSet *allReloadedSections = [_ASHierarchySectionChange allIndexesInSectionChanges:_reloadSectionChanges]; + + NSInteger newSectionCount = _newItemCounts.size(); + NSInteger oldSectionCount = _oldItemCounts.size(); + + NSInteger insertedSectionCount = _insertedSections.count; + NSInteger deletedSectionCount = _deletedSections.count; + // Assert that the new section count is correct. + if (newSectionCount != oldSectionCount + insertedSectionCount - deletedSectionCount) { + ASFailUpdateValidation(@"Invalid number of sections. The number of sections after the update (%zd) must be equal to the number of sections before the update (%zd) plus or minus the number of sections inserted or deleted (%zu inserted, %zu deleted)", newSectionCount, oldSectionCount, insertedSectionCount, deletedSectionCount); + return; + } + + // Assert that no invalid deletes/reloads happened. + NSInteger invalidSectionDelete = NSNotFound; + if (oldSectionCount == 0) { + invalidSectionDelete = _deletedSections.firstIndex; + } else { + invalidSectionDelete = [_deletedSections indexGreaterThanIndex:oldSectionCount - 1]; + } + if (invalidSectionDelete != NSNotFound) { + ASFailUpdateValidation(@"Attempt to delete section %zd but there are only %zd sections before the update.", invalidSectionDelete, oldSectionCount); + return; + } + + for (_ASHierarchyItemChange *change in _deleteItemChanges) { + for (NSIndexPath *indexPath in change.indexPaths) { + // Assert that item delete happened in a valid section. + NSInteger section = indexPath.section; + NSInteger item = indexPath.item; + if (section >= oldSectionCount) { + ASFailUpdateValidation(@"Attempt to delete item %zd from section %zd, but there are only %zd sections before the update.", item, section, oldSectionCount); + return; + } + + // Assert that item delete happened to a valid item. + NSInteger oldItemCount = _oldItemCounts[section]; + if (item >= oldItemCount) { + ASFailUpdateValidation(@"Attempt to delete item %zd from section %zd, which only contains %zd items before the update.", item, section, oldItemCount); + return; + } + } + } + + for (_ASHierarchyItemChange *change in _insertItemChanges) { + for (NSIndexPath *indexPath in change.indexPaths) { + NSInteger section = indexPath.section; + NSInteger item = indexPath.item; + // Assert that item insert happened in a valid section. + if (section >= newSectionCount) { + ASFailUpdateValidation(@"Attempt to insert item %zd into section %zd, but there are only %zd sections after the update.", item, section, newSectionCount); + return; + } + + // Assert that item delete happened to a valid item. + NSInteger newItemCount = _newItemCounts[section]; + if (item >= newItemCount) { + ASFailUpdateValidation(@"Attempt to insert item %zd into section %zd, which only contains %zd items after the update.", item, section, newItemCount); + return; + } + } + } + + // Assert that no sections were inserted out of bounds. + NSInteger invalidSectionInsert = NSNotFound; + if (newSectionCount == 0) { + invalidSectionInsert = _insertedSections.firstIndex; + } else { + invalidSectionInsert = [_insertedSections indexGreaterThanIndex:newSectionCount - 1]; + } + if (invalidSectionInsert != NSNotFound) { + ASFailUpdateValidation(@"Attempt to insert section %zd but there are only %zd sections after the update.", invalidSectionInsert, newSectionCount); + return; + } + + for (NSUInteger oldSection = 0; oldSection < oldSectionCount; oldSection++) { + NSInteger oldItemCount = _oldItemCounts[oldSection]; + // If section was reloaded, ignore. + if ([allReloadedSections containsIndex:oldSection]) { + continue; + } + + // If section was deleted, ignore. + NSUInteger newSection = [self newSectionForOldSection:oldSection]; + if (newSection == NSNotFound) { + continue; + } + + NSIndexSet *originalInsertedItems = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeOriginalInsert inSection:newSection]; + NSIndexSet *originalDeletedItems = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeOriginalDelete inSection:oldSection]; + NSIndexSet *reloadedItems = [self indexesForItemChangesOfType:_ASHierarchyChangeTypeReload inSection:oldSection]; + + // Assert that no reloaded items were deleted. + NSInteger deletedReloadedItem = [originalDeletedItems as_intersectionWithIndexes:reloadedItems].firstIndex; + if (deletedReloadedItem != NSNotFound) { + ASFailUpdateValidation(@"Attempt to delete and reload the same item at index path %@", [NSIndexPath indexPathForItem:deletedReloadedItem inSection:oldSection]); + return; + } + + // Assert that the new item count is correct. + NSInteger newItemCount = _newItemCounts[newSection]; + NSInteger insertedItemCount = originalInsertedItems.count; + NSInteger deletedItemCount = originalDeletedItems.count; + if (newItemCount != oldItemCount + insertedItemCount - deletedItemCount) { + ASFailUpdateValidation(@"Invalid number of items in section %zd. The number of items after the update (%zd) must be equal to the number of items before the update (%zd) plus or minus the number of items inserted or deleted (%zd inserted, %zd deleted).", oldSection, newItemCount, oldItemCount, insertedItemCount, deletedItemCount); + return; + } } } @@ -300,14 +479,33 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) return self; } -+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes +- (_ASHierarchySectionChange *)changeByFinalizingType { - if (changes.count < 1) { + _ASHierarchyChangeType newType; + switch (_changeType) { + case _ASHierarchyChangeTypeOriginalInsert: + newType = _ASHierarchyChangeTypeInsert; + break; + case _ASHierarchyChangeTypeOriginalDelete: + newType = _ASHierarchyChangeTypeDelete; + break; + default: + ASFailUpdateValidation(@"Attempt to finalize section change of invalid type %@.", NSStringFromASHierarchyChangeType(_changeType)); + return self; + } + return [[_ASHierarchySectionChange alloc] initWithChangeType:newType indexSet:_indexSet animationOptions:_animationOptions]; +} + ++ (void)sortAndCoalesceSectionChanges:(NSMutableArray<_ASHierarchySectionChange *> *)changes +{ + _ASHierarchySectionChange *firstChange = changes.firstObject; + if (firstChange == nil) { return; } + _ASHierarchyChangeType type = [firstChange changeType]; - _ASHierarchyChangeType type = [changes.firstObject changeType]; - + ASDisplayNodeAssert(ASHierarchyChangeTypeIsFinal(type), @"Attempt to sort and coalesce section changes of intermediary type %@. Why?", NSStringFromASHierarchyChangeType(type)); + // Lookup table [Int: AnimationOptions] __block std::unordered_map animationOptions; @@ -326,12 +524,12 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) } // Create new changes by grouping sorted changes by animation option - NSMutableArray *result = [NSMutableArray new]; + NSMutableArray *result = [[NSMutableArray alloc] init]; __block ASDataControllerAnimationOptions currentOptions = 0; NSMutableIndexSet *currentIndexes = [NSMutableIndexSet indexSet]; - BOOL reverse = type == _ASHierarchyChangeTypeDelete; + BOOL reverse = type == _ASHierarchyChangeTypeDelete || type == _ASHierarchyChangeTypeOriginalDelete; NSEnumerationOptions options = reverse ? NSEnumerationReverse : kNilOptions; [allIndexes enumerateRangesWithOptions:options usingBlock:^(NSRange range, BOOL * _Nonnull stop) { @@ -423,19 +621,37 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) return sectionToIndexSetMap; } -+ (void)sortAndCoalesceChanges:(NSMutableArray *)changes ignoringChangesInSections:(NSIndexSet *)ignoredSections +- (_ASHierarchyItemChange *)changeByFinalizingType +{ + _ASHierarchyChangeType newType; + switch (_changeType) { + case _ASHierarchyChangeTypeOriginalInsert: + newType = _ASHierarchyChangeTypeInsert; + break; + case _ASHierarchyChangeTypeOriginalDelete: + newType = _ASHierarchyChangeTypeDelete; + break; + default: + ASFailUpdateValidation(@"Attempt to finalize item change of invalid type %@.", NSStringFromASHierarchyChangeType(_changeType)); + return self; + } + return [[_ASHierarchyItemChange alloc] initWithChangeType:newType indexPaths:_indexPaths animationOptions:_animationOptions presorted:YES]; +} + ++ (void)sortAndCoalesceItemChanges:(NSMutableArray<_ASHierarchyItemChange *> *)changes ignoringChangesInSections:(NSIndexSet *)ignoredSections { if (changes.count < 1) { return; } _ASHierarchyChangeType type = [changes.firstObject changeType]; - + ASDisplayNodeAssert(ASHierarchyChangeTypeIsFinal(type), @"Attempt to sort and coalesce item changes of intermediary type %@. Why?", NSStringFromASHierarchyChangeType(type)); + // Lookup table [NSIndexPath: AnimationOptions] NSMutableDictionary *animationOptions = [NSMutableDictionary new]; // All changed index paths, sorted - NSMutableArray *allIndexPaths = [NSMutableArray new]; + NSMutableArray *allIndexPaths = [[NSMutableArray alloc] init]; for (_ASHierarchyItemChange *change in changes) { for (NSIndexPath *indexPath in change.indexPaths) { @@ -450,7 +666,7 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType) [allIndexPaths sortUsingSelector:sorting]; // Create new changes by grouping sorted changes by animation option - NSMutableArray *result = [NSMutableArray new]; + NSMutableArray *result = [[NSMutableArray alloc] init]; ASDataControllerAnimationOptions currentOptions = 0; NSMutableArray *currentIndexPaths = [NSMutableArray array]; diff --git a/AsyncDisplayKitTests/ASCollectionViewTests.m b/AsyncDisplayKitTests/ASCollectionViewTests.mm similarity index 70% rename from AsyncDisplayKitTests/ASCollectionViewTests.m rename to AsyncDisplayKitTests/ASCollectionViewTests.mm index f20b0665c8..9986f0cd3a 100644 --- a/AsyncDisplayKitTests/ASCollectionViewTests.m +++ b/AsyncDisplayKitTests/ASCollectionViewTests.mm @@ -14,6 +14,8 @@ #import "ASCollectionViewFlowLayoutInspector.h" #import "ASCellNode.h" #import "ASCollectionNode.h" +#import "ASDisplayNode+Beta.h" +#import @interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode @@ -33,17 +35,18 @@ @interface ASCollectionViewTestDelegate : NSObject -@property (nonatomic, assign) NSInteger numberOfSections; -@property (nonatomic, assign) NSInteger numberOfItemsInSection; - @end -@implementation ASCollectionViewTestDelegate +@implementation ASCollectionViewTestDelegate { + @package + std::vector _itemCounts; +} - (id)initWithNumberOfSections:(NSInteger)numberOfSections numberOfItemsInSection:(NSInteger)numberOfItemsInSection { if (self = [super init]) { - _numberOfSections = numberOfSections; - _numberOfItemsInSection = numberOfItemsInSection; + for (NSInteger i = 0; i < numberOfSections; i++) { + _itemCounts.push_back(numberOfItemsInSection); + } } return self; @@ -66,11 +69,11 @@ } - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { - return self.numberOfSections; + return _itemCounts.size(); } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { - return self.numberOfItemsInSection; + return _itemCounts[section]; } @end @@ -84,23 +87,21 @@ @implementation ASCollectionViewTestController -- (void)viewDidLoad { - [super viewDidLoad]; - - self.asyncDelegate = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:10 numberOfItemsInSection:10]; - - self.collectionView = [[ASCollectionView alloc] initWithFrame:self.view.bounds - collectionViewLayout:[UICollectionViewFlowLayout new]]; - self.collectionView.asyncDataSource = self.asyncDelegate; - self.collectionView.asyncDelegate = self.asyncDelegate; - - [self.view addSubview:self.collectionView]; -} - -- (void)viewWillLayoutSubviews { - [super viewWillLayoutSubviews]; - - self.collectionView.frame = self.view.bounds; +- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Populate these immediately so that they're not unexpectedly nil during tests. + self.asyncDelegate = [[ASCollectionViewTestDelegate alloc] initWithNumberOfSections:10 numberOfItemsInSection:10]; + + self.collectionView = [[ASCollectionView alloc] initWithFrame:self.view.bounds + collectionViewLayout:[UICollectionViewFlowLayout new]]; + self.collectionView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.collectionView.asyncDataSource = self.asyncDelegate; + self.collectionView.asyncDelegate = self.asyncDelegate; + + [self.view addSubview:self.collectionView]; + } + return self; } @end @@ -252,4 +253,108 @@ XCTAssert([node conformsToProtocol:@protocol(ASRangeControllerUpdateRangeProtocol)]); } +#pragma mark - Update Validations + +#define updateValidationTestPrologue \ + [ASDisplayNode setSuppressesInvalidCollectionUpdateExceptions:NO];\ + ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];\ + __unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\ + __unused ASCollectionView *cv = testController.collectionView;\ + UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\ + window.rootViewController = testController;\ + \ + [testController.collectionView reloadDataImmediately];\ + [testController.collectionView layoutIfNeeded]; + +- (void)testThatSubmittingAValidInsertDoesNotThrowAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + del->_itemCounts[sectionCount - 1]++; + XCTAssertNoThrow([cv insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount - 1] ]]); +} + +- (void)testThatSubmittingAValidReloadDoesNotThrowAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + XCTAssertNoThrow([cv reloadItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount - 1] ]]); +} + +- (void)testThatSubmittingAnInvalidInsertThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + XCTAssertThrows([cv insertItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount + 1] ]]); +} + +- (void)testThatSubmittingAnInvalidDeleteThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + XCTAssertThrows([cv deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:sectionCount + 1] ]]); +} + +- (void)testThatDeletingAndReloadingTheSameItemThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv performBatchUpdates:^{ + NSArray *indexPaths = @[ [NSIndexPath indexPathForItem:0 inSection:0] ]; + [cv deleteItemsAtIndexPaths:indexPaths]; + [cv reloadItemsAtIndexPaths:indexPaths]; + } completion:nil]); +} + +- (void)testThatHavingAnIncorrectSectionCountThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv deleteSections:[NSIndexSet indexSetWithIndex:0]]); +} + +- (void)testThatHavingAnIncorrectItemCountThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv deleteItemsAtIndexPaths:@[ [NSIndexPath indexPathForItem:0 inSection:0] ]]); +} + +- (void)testThatHavingAnIncorrectItemCountWithNoUpdatesThrowsAnException +{ + updateValidationTestPrologue + + XCTAssertThrows([cv performBatchUpdates:^{ + del->_itemCounts[0]++; + } completion:nil]); +} + +- (void)testThatInsertingAnInvalidSectionThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + del->_itemCounts.push_back(10); + XCTAssertThrows([cv performBatchUpdates:^{ + [cv insertSections:[NSIndexSet indexSetWithIndex:sectionCount + 1]]; + } completion:nil]); +} + +- (void)testThatDeletingAndReloadingASectionThrowsAnException +{ + updateValidationTestPrologue + NSInteger sectionCount = del->_itemCounts.size(); + + del->_itemCounts.pop_back(); + XCTAssertThrows([cv performBatchUpdates:^{ + NSIndexSet *sections = [NSIndexSet indexSetWithIndex:sectionCount - 1]; + [cv reloadSections:sections]; + [cv deleteSections:sections]; + } completion:nil]); +} + @end diff --git a/Base/ASAssert.h b/Base/ASAssert.h index d3f88a44d3..2df66b37a7 100644 --- a/Base/ASAssert.h +++ b/Base/ASAssert.h @@ -48,8 +48,8 @@ #define ASDisplayNodeAssertFalse(condition) ASDisplayNodeAssertWithSignal(!(condition), nil, nil) #define ASDisplayNodeCAssertFalse(condition) ASDisplayNodeCAssertWithSignal(!(condition), nil, nil) -#define ASDisplayNodeFailAssert(description, ...) ASDisplayNodeAssertWithSignal(NO, nil, (description), ##__VA_ARGS__) -#define ASDisplayNodeCFailAssert(description, ...) ASDisplayNodeCAssertWithSignal(NO, nil, (description), ##__VA_ARGS__) +#define ASDisplayNodeFailAssert(description, ...) ASDisplayNodeAssertWithSignal(NO, (description), ##__VA_ARGS__) +#define ASDisplayNodeCFailAssert(description, ...) ASDisplayNodeCAssertWithSignal(NO, (description), ##__VA_ARGS__) #define ASDisplayNodeConditionalAssert(shouldTestCondition, condition, description, ...) ASDisplayNodeAssert((!(shouldTestCondition) || (condition)), nil, (description), ##__VA_ARGS__) #define ASDisplayNodeConditionalCAssert(shouldTestCondition, condition, description, ...) ASDisplayNodeCAssert((!(shouldTestCondition) || (condition)), nil, (description), ##__VA_ARGS__)