mirror of
https://github.com/HackPlan/AsyncDisplayKit.git
synced 2026-04-24 03:45:58 +08:00
[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:
@@ -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 */,
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
@@ -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__)
|
||||
|
||||
Reference in New Issue
Block a user