[ASDataController] Add validation logic to the change set to throw exceptions on invalid updates (#1894)

[_ASHierarchyChangeSet] Oopsy daisy

[ASDataController] Tweak our update validation

[ASHierarchyChangeSet] Fix bugs

Finish up some stuff

[ASDataController] Put some stuff back

[ASChangeSetDataController] Always use changeset

[ASDataController] Put other stuff back

[_ASHierarchyChangeSet] Use fast enumeration

[_ASHierarchyChangeSet] Fix assertion format strings, return on fail so we don't crash in production

[ASDataController] Store data source item counts as vector rather than NSArray

[ASDataController] Build some tests for the update validation

[ASDataController] Fix issues with update validation

Get rid of new file

[ASDataController] Suppress changeset validation before initial reload

[ASDataController] Make invalid update log vs. exception publicly toggleable
This commit is contained in:
Adlai Holler
2016-07-21 14:37:51 -07:00
committed by GitHub
parent 0c70bca2bd
commit 8cde594de3
11 changed files with 567 additions and 147 deletions

View File

@@ -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 = "<group>"; };
9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASViewController.mm; sourceTree = "<group>"; };
9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableNode.mm; sourceTree = "<group>"; };
9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionViewTests.m; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionViewTests.mm; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.objc; };
A2763D771CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASPINRemoteImageDownloader.h; path = Details/ASPINRemoteImageDownloader.h; sourceTree = "<group>"; };
A2763D781CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASPINRemoteImageDownloader.m; path = Details/ASPINRemoteImageDownloader.m; sourceTree = "<group>"; };
A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASTextKitFontSizeAdjuster.h; path = TextKit/ASTextKitFontSizeAdjuster.h; sourceTree = "<group>"; };
A373200E1C571B050011FC94 /* ASTextNode+Beta.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASTextNode+Beta.h"; sourceTree = "<group>"; };
AC026B571BD3F61800BBC17E /* ASStaticLayoutSpecSnapshotTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASStaticLayoutSpecSnapshotTests.m; sourceTree = "<group>"; };
AC026B671BD57D6F00BBC17E /* ASChangeSetDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASChangeSetDataController.h; sourceTree = "<group>"; };
AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASChangeSetDataController.m; sourceTree = "<group>"; };
AC026B681BD57D6F00BBC17E /* ASChangeSetDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASChangeSetDataController.mm; sourceTree = "<group>"; };
AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASHierarchyChangeSet.h; sourceTree = "<group>"; };
AC026B6E1BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASHierarchyChangeSet.mm; sourceTree = "<group>"; };
AC21EC0F1B3D0BF600C8B19A /* ASStackLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASStackLayoutDefines.h; path = AsyncDisplayKit/Layout/ASStackLayoutDefines.h; sourceTree = "<group>"; };
@@ -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 */,

View File

@@ -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 */

View File

@@ -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);

View File

@@ -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

View File

@@ -9,6 +9,7 @@
//
#pragma once
#import <vector>
@class ASIndexedNodeContext;
@@ -33,6 +34,21 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray<ASCellNode *> *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<NSInteger>)itemCountsFromDataSource;
#pragma mark - Node sizing
/**

View File

@@ -124,6 +124,15 @@ FOUNDATION_EXPORT NSString * const ASDataControllerRowNodeKind;
*/
@property (nonatomic, weak) id<ASDataControllerEnvironmentDelegate> 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;

View File

@@ -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<NSInteger> _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<ASIndexedNodeContext *> *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<NSInteger>)itemCountsFromDataSource
{
ASDisplayNodeAssertMainThread();
if (NO == _itemCountsFromDataSourceAreValid) {
id<ASDataControllerSource> source = self.dataSource;
NSInteger sectionCount = [source numberOfSectionsInDataController:self];
std::vector<NSInteger> 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

View File

@@ -11,17 +11,51 @@
//
#import <Foundation/Foundation.h>
#import <vector>
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<NSInteger>)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<NSInteger>)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;

View File

@@ -14,15 +14,37 @@
#import "ASInternalHelpers.h"
#import "NSIndexSet+ASHelpers.h"
#import "ASAssert.h"
#import "ASDisplayNode+Beta.h"
#import <unordered_map>
#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<NSInteger> _oldItemCounts;
std::vector<NSInteger> _newItemCounts;
}
- (instancetype)init
{
ASFailUpdateValidation(@"_ASHierarchyChangeSet: -init is not supported. Call -initWithOldData:");
return [self initWithOldData:std::vector<NSInteger>()];
}
- (instancetype)initWithOldData:(std::vector<NSInteger>)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<NSInteger>)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<NSUInteger, ASDataControllerAnimationOptions> 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];

View File

@@ -14,6 +14,8 @@
#import "ASCollectionViewFlowLayoutInspector.h"
#import "ASCellNode.h"
#import "ASCollectionNode.h"
#import "ASDisplayNode+Beta.h"
#import <vector>
@interface ASTextCellNodeWithSetSelectedCounter : ASTextCellNode
@@ -33,17 +35,18 @@
@interface ASCollectionViewTestDelegate : NSObject <ASCollectionViewDataSource, ASCollectionViewDelegate>
@property (nonatomic, assign) NSInteger numberOfSections;
@property (nonatomic, assign) NSInteger numberOfItemsInSection;
@end
@implementation ASCollectionViewTestDelegate
@implementation ASCollectionViewTestDelegate {
@package
std::vector<NSInteger> _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

View File

@@ -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__)