Overhaul ASDataController and extensively test ASTableView.

This diff resolves all known consistency issues with ASTableView and ASCollectionView.
It includes significantly more aggressive thrash-testing in ASTableViewStressTest,
which now passes on a variety of device and simulator configurations.  It also updates
the unit tests run on every commit to ensure any regression is caught quickly.

A few of the salient changes in this diff:
- ASTableView now uses Rene's ASCollectionViewLayoutController, and actually uses a
UICollectionViewFlowLayout without any UICollectionView.  This resolves an issue where
ASFlowLayoutController was generating slightly out-of-bounds indicies when programmatically
scrolling past the end of the table content.  Because the custom implementation is likely
faster, I will revisit this later with profiling and possibly returning to the custom impl.
- There is now a second copy of the _nodes array maintained by ASDataController.  It shares
the same node instances, but this does add some overhead to manipulating the arrays. I've
filed a task to follow up with optimization, as there are several great opportunities to
make it faster.  However, I don't believe the overhead is a significant issue, and it does
guarantee correctness in even the toughest app usage scenarios.
- ASDataController no longer supports calling its delegate /before/ edit operations.  No
other class was relying on this behavior, and it would be unusual for an app developer to
use ASDataController directly.  However, it is possible that someone with a custom view
that integrates with ASDataController and ASRangeController could be affected by this.
- Further cleanup of organization, naming, additional comments, reduced code length
wherever possible.  Overall, significantly more accessible to a new reader.
This commit is contained in:
Scott Goodson
2015-06-28 18:03:45 -07:00
parent a654d880c3
commit 57465c7fd3
12 changed files with 479 additions and 411 deletions

View File

@@ -10,7 +10,7 @@
#import "ASAssert.h" #import "ASAssert.h"
#import "ASDataController.h" #import "ASDataController.h"
#import "ASFlowLayoutController.h" #import "ASCollectionViewLayoutController.h"
#import "ASLayoutController.h" #import "ASLayoutController.h"
#import "ASRangeController.h" #import "ASRangeController.h"
#import "ASDisplayNodeInternal.h" #import "ASDisplayNodeInternal.h"
@@ -117,7 +117,7 @@ static BOOL _isInterceptedSelector(SEL sel)
_ASTableViewProxy *_proxyDelegate; _ASTableViewProxy *_proxyDelegate;
ASDataController *_dataController; ASDataController *_dataController;
ASFlowLayoutController *_layoutController; ASCollectionViewLayoutController *_layoutController;
ASRangeController *_rangeController; ASRangeController *_rangeController;
@@ -159,7 +159,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled - (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled
{ {
_layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
_layoutController = [[ASCollectionViewLayoutController alloc] initWithScrollView:self collectionViewLayout:flowLayout];
_rangeController = [[ASRangeController alloc] init]; _rangeController = [[ASRangeController alloc] init];
_rangeController.layoutController = _layoutController; _rangeController.layoutController = _layoutController;
@@ -412,20 +413,12 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
{ {
CGPoint scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; CGPoint scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview];
ASScrollDirection direction = ASScrollDirectionNone; ASScrollDirection direction = ASScrollDirectionNone;
if (_layoutController.layoutDirection == ASFlowLayoutDirectionHorizontal) { if (scrollVelocity.y > 0) {
if (scrollVelocity.x > 0) { direction = ASScrollDirectionDown;
direction = ASScrollDirectionRight;
} else if (scrollVelocity.x < 0) {
direction = ASScrollDirectionLeft;
}
} else { } else {
if (scrollVelocity.y > 0) { direction = ASScrollDirectionUp;
direction = ASScrollDirectionDown;
} else {
direction = ASScrollDirectionUp;
}
} }
return direction; return direction;
} }

View File

@@ -14,5 +14,6 @@
@interface ASCollectionViewLayoutController : ASAbstractLayoutController @interface ASCollectionViewLayoutController : ASAbstractLayoutController
- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView; - (instancetype)initWithCollectionView:(ASCollectionView *)collectionView;
- (instancetype)initWithScrollView:(UIScrollView *)scrollView collectionViewLayout:(UICollectionViewLayout *)layout;
@end @end

View File

@@ -21,7 +21,8 @@ struct ASDirectionalScreenfulBuffer {
typedef struct ASDirectionalScreenfulBuffer ASDirectionalScreenfulBuffer; typedef struct ASDirectionalScreenfulBuffer ASDirectionalScreenfulBuffer;
ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDirection scrollDirection, ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDirection scrollDirection,
ASRangeTuningParameters rangeTuningParameters) { ASRangeTuningParameters rangeTuningParameters)
{
ASDirectionalScreenfulBuffer horizontalBuffer = {0, 0}; ASDirectionalScreenfulBuffer horizontalBuffer = {0, 0};
BOOL movingRight = ASScrollDirectionContainsRight(scrollDirection); BOOL movingRight = ASScrollDirectionContainsRight(scrollDirection);
horizontalBuffer.positiveDirection = movingRight ? rangeTuningParameters.leadingBufferScreenfuls : horizontalBuffer.positiveDirection = movingRight ? rangeTuningParameters.leadingBufferScreenfuls :
@@ -32,7 +33,8 @@ ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDire
} }
ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferVertical(ASScrollDirection scrollDirection, ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferVertical(ASScrollDirection scrollDirection,
ASRangeTuningParameters rangeTuningParameters) { ASRangeTuningParameters rangeTuningParameters)
{
ASDirectionalScreenfulBuffer verticalBuffer = {0, 0}; ASDirectionalScreenfulBuffer verticalBuffer = {0, 0};
BOOL movingDown = ASScrollDirectionContainsDown(scrollDirection); BOOL movingDown = ASScrollDirectionContainsDown(scrollDirection);
verticalBuffer.positiveDirection = movingDown ? rangeTuningParameters.leadingBufferScreenfuls : verticalBuffer.positiveDirection = movingDown ? rangeTuningParameters.leadingBufferScreenfuls :
@@ -52,44 +54,64 @@ typedef struct ASRangeGeometry ASRangeGeometry;
#pragma mark - #pragma mark -
#pragma mark ASCollectionViewLayoutController #pragma mark ASCollectionViewLayoutController
@interface ASCollectionViewLayoutController () { @interface ASCollectionViewLayoutController ()
ASCollectionView * __weak _collectionView; {
UIScrollView * __weak _scrollView;
UICollectionViewLayout * __strong _collectionViewLayout;
std::vector<CGRect> _updateRangeBoundsIndexedByRangeType; std::vector<CGRect> _updateRangeBoundsIndexedByRangeType;
ASScrollDirection _scrollableDirections;
} }
@end @end
@implementation ASCollectionViewLayoutController @implementation ASCollectionViewLayoutController
- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView { - (instancetype)initWithCollectionView:(ASCollectionView *)collectionView
{
if (!(self = [super init])) { if (!(self = [super init])) {
return nil; return nil;
} }
_collectionView = collectionView;
_scrollableDirections = [collectionView scrollableDirections];
_scrollView = collectionView;
_collectionViewLayout = [collectionView collectionViewLayout];
_updateRangeBoundsIndexedByRangeType = std::vector<CGRect>(ASLayoutRangeTypeCount); _updateRangeBoundsIndexedByRangeType = std::vector<CGRect>(ASLayoutRangeTypeCount);
return self; return self;
} }
- (instancetype)initWithScrollView:(UIScrollView *)scrollView collectionViewLayout:(UICollectionViewLayout *)layout
{
if (!(self = [super init])) {
return nil;
}
_scrollableDirections = ASScrollDirectionVerticalDirections;
_scrollView = scrollView;
_collectionViewLayout = layout;
_updateRangeBoundsIndexedByRangeType = std::vector<CGRect>(ASLayoutRangeTypeCount);
return self;
}
#pragma mark - #pragma mark -
#pragma mark Index Paths in Range #pragma mark Index Paths in Range
- (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection - (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection
viewportSize:(CGSize)viewportSize viewportSize:(CGSize)viewportSize
rangeType:(ASLayoutRangeType)rangeType { rangeType:(ASLayoutRangeType)rangeType
{
ASRangeGeometry rangeGeometry = [self rangeGeometryWithScrollDirection:scrollDirection ASRangeGeometry rangeGeometry = [self rangeGeometryWithScrollDirection:scrollDirection
collectionView:_collectionView
rangeTuningParameters:[self tuningParametersForRangeType:rangeType]]; rangeTuningParameters:[self tuningParametersForRangeType:rangeType]];
_updateRangeBoundsIndexedByRangeType[rangeType] = rangeGeometry.updateBounds; _updateRangeBoundsIndexedByRangeType[rangeType] = rangeGeometry.updateBounds;
return [self indexPathsForItemsWithinRangeBounds:rangeGeometry.rangeBounds collectionView:_collectionView]; return [self indexPathsForItemsWithinRangeBounds:rangeGeometry.rangeBounds];
} }
- (ASRangeGeometry)rangeGeometryWithScrollDirection:(ASScrollDirection)scrollDirection - (ASRangeGeometry)rangeGeometryWithScrollDirection:(ASScrollDirection)scrollDirection
collectionView:(ASCollectionView *)collectionView rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters
rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters { {
CGRect rangeBounds = collectionView.bounds; CGRect rangeBounds = _scrollView.bounds;
CGRect updateBounds = collectionView.bounds; CGRect updateBounds = _scrollView.bounds;
ASScrollDirection scrollableDirections = [collectionView scrollableDirections];
BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(scrollableDirections); BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(_scrollableDirections);
if (canScrollHorizontally) { if (canScrollHorizontally) {
ASDirectionalScreenfulBuffer horizontalBuffer = ASDirectionalScreenfulBufferHorizontal(scrollDirection, ASDirectionalScreenfulBuffer horizontalBuffer = ASDirectionalScreenfulBufferHorizontal(scrollDirection,
rangeTuningParameters); rangeTuningParameters);
@@ -102,7 +124,7 @@ typedef struct ASRangeGeometry ASRangeGeometry;
MIN(horizontalBuffer.positiveDirection * 0.5, 0.95)); MIN(horizontalBuffer.positiveDirection * 0.5, 0.95));
} }
BOOL canScrollVertically = ASScrollDirectionContainsVerticalDirection(scrollableDirections); BOOL canScrollVertically = ASScrollDirectionContainsVerticalDirection(_scrollableDirections);
if (canScrollVertically) { if (canScrollVertically) {
ASDirectionalScreenfulBuffer verticalBuffer = ASDirectionalScreenfulBufferVertical(scrollDirection, ASDirectionalScreenfulBuffer verticalBuffer = ASDirectionalScreenfulBufferVertical(scrollDirection,
rangeTuningParameters); rangeTuningParameters);
@@ -118,9 +140,10 @@ typedef struct ASRangeGeometry ASRangeGeometry;
return {rangeBounds, updateBounds}; return {rangeBounds, updateBounds};
} }
- (NSSet *)indexPathsForItemsWithinRangeBounds:(CGRect)rangeBounds collectionView:(ASCollectionView *)collectionView { - (NSSet *)indexPathsForItemsWithinRangeBounds:(CGRect)rangeBounds
{
NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; NSMutableSet *indexPathSet = [[NSMutableSet alloc] init];
NSArray *layoutAttributes = [collectionView.collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:rangeBounds];
for (UICollectionViewLayoutAttributes *la in layoutAttributes) { for (UICollectionViewLayoutAttributes *la in layoutAttributes) {
[indexPathSet addObject:la.indexPath]; [indexPathSet addObject:la.indexPath];
} }
@@ -132,13 +155,14 @@ typedef struct ASRangeGeometry ASRangeGeometry;
- (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths - (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths
viewportSize:(CGSize)viewportSize viewportSize:(CGSize)viewportSize
rangeType:(ASLayoutRangeType)rangeType { rangeType:(ASLayoutRangeType)rangeType
{
CGRect updateRangeBounds = _updateRangeBoundsIndexedByRangeType[rangeType]; CGRect updateRangeBounds = _updateRangeBoundsIndexedByRangeType[rangeType];
if (CGRectIsEmpty(updateRangeBounds)) { if (CGRectIsEmpty(updateRangeBounds)) {
return YES; return YES;
} }
CGRect currentBounds = _collectionView.bounds; CGRect currentBounds = _scrollView.bounds;
if (CGRectIsEmpty(currentBounds)) { if (CGRectIsEmpty(currentBounds)) {
currentBounds = CGRectMake(0, 0, viewportSize.width, viewportSize.height); currentBounds = CGRectMake(0, 0, viewportSize.width, viewportSize.height);
} }

View File

@@ -70,25 +70,21 @@ typedef NSUInteger ASDataControllerAnimationOptions;
/** /**
Called for insertion of elements. Called for insertion of elements.
*/ */
- (void)dataController:(ASDataController *)dataController willInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/** /**
Called for deletion of elements. Called for deletion of elements.
*/ */
- (void)dataController:(ASDataController *)dataController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/** /**
Called for insertion of sections. Called for insertion of sections.
*/ */
- (void)dataController:(ASDataController *)dataController willInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/** /**
Called for deletion of sections. Called for deletion of sections.
*/ */
- (void)dataController:(ASDataController *)dataController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
- (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
@end @end
@@ -117,7 +113,7 @@ typedef NSUInteger ASDataControllerAnimationOptions;
* Designated iniailizer. * Designated iniailizer.
* *
* @param asyncDataFetchingEnabled Enable the data fetching in async mode. * @param asyncDataFetchingEnabled Enable the data fetching in async mode.
*
* @discussion If enabled, we will fetch data through `dataController:nodeAtIndexPath:` and `dataController:rowsInSection:` in background thread. * @discussion If enabled, we will fetch data through `dataController:nodeAtIndexPath:` and `dataController:rowsInSection:` in background thread.
* Otherwise, the methods will be invoked synchronically in calling thread. Enabling data fetching in async mode could avoid blocking main thread * Otherwise, the methods will be invoked synchronically in calling thread. Enabling data fetching in async mode could avoid blocking main thread
* while allocating cell on main thread, which is frequently reported issue for handling large scale data. On another hand, the application code * while allocating cell on main thread, which is frequently reported issue for handling large scale data. On another hand, the application code
@@ -127,7 +123,11 @@ typedef NSUInteger ASDataControllerAnimationOptions;
*/ */
- (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled; - (instancetype)initWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled;
/** @name Initial loading */ /** @name Initial loading
*
* @discussion This method allows choosing an animation style for the first load of content. It is typically used just once,
* for example in viewWillAppear:, to specify an animation option for the information already present in the asyncDataSource.
*/
- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;

View File

@@ -21,16 +21,16 @@ const static NSUInteger kASDataControllerSizingCountPerProcessor = 5;
static void *kASSizingQueueContext = &kASSizingQueueContext; static void *kASSizingQueueContext = &kASSizingQueueContext;
@interface ASDataController () { @interface ASDataController () {
NSMutableArray *_nodes; NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this.
NSMutableArray *_pendingBlocks; NSMutableArray *_editingNodes; // Modified on _editingTransactionQueue only. Updates propogated to _completedNodes.
NSMutableArray *_pendingEditCommandBlocks; // To be run on the main thread. Handles begin/endUpdates tracking.
NSOperationQueue *_editingTransactionQueue; // Serial background queue. Dispatches concurrent layout and manages _editingNodes.
BOOL _asyncDataFetchingEnabled; BOOL _asyncDataFetchingEnabled;
BOOL _delegateWillInsertNodes;
BOOL _delegateDidInsertNodes; BOOL _delegateDidInsertNodes;
BOOL _delegateWillDeleteNodes;
BOOL _delegateDidDeleteNodes; BOOL _delegateDidDeleteNodes;
BOOL _delegateWillInsertSections;
BOOL _delegateDidInsertSections; BOOL _delegateDidInsertSections;
BOOL _delegateWillDeleteSections;
BOOL _delegateDidDeleteSections; BOOL _delegateDidDeleteSections;
} }
@@ -48,8 +48,16 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
return nil; return nil;
} }
_nodes = [NSMutableArray array]; _completedNodes = [NSMutableArray array];
_pendingBlocks = [NSMutableArray array]; _editingNodes = [NSMutableArray array];
_pendingEditCommandBlocks = [NSMutableArray array];
_editingTransactionQueue = [[NSOperationQueue alloc] init];
_editingTransactionQueue.qualityOfService = NSQualityOfServiceUserInitiated;
_editingTransactionQueue.maxConcurrentOperationCount = 1; // Serial queue
_editingTransactionQueue.name = @"org.AsyncDisplayKit.ASDataController.editingTransactionQueue";
_batchUpdateCounter = 0; _batchUpdateCounter = 0;
_asyncDataFetchingEnabled = asyncDataFetchingEnabled; _asyncDataFetchingEnabled = asyncDataFetchingEnabled;
@@ -65,18 +73,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
_delegate = delegate; _delegate = delegate;
// Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later. // Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later.
_delegateWillInsertNodes = [_delegate respondsToSelector:@selector(dataController:willInsertNodes:atIndexPaths:withAnimationOptions:)];
_delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)]; _delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)];
_delegateWillDeleteNodes = [_delegate respondsToSelector:@selector(dataController:willDeleteNodesAtIndexPaths:withAnimationOptions:)];
_delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:withAnimationOptions:)]; _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:withAnimationOptions:)];
_delegateWillInsertSections = [_delegate respondsToSelector:@selector(dataController:willInsertSections:atIndexSet:withAnimationOptions:)];
_delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)];
_delegateWillDeleteSections = [_delegate respondsToSelector:@selector(dataController:willDeleteSectionsAtIndexSet:withAnimationOptions:)];
_delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)];
} }
#pragma mark - Queue Management
+ (NSUInteger)parallelProcessorCount + (NSUInteger)parallelProcessorCount
{ {
static NSUInteger parallelProcessorCount; static NSUInteger parallelProcessorCount;
@@ -89,64 +91,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
return parallelProcessorCount; return parallelProcessorCount;
} }
+ (dispatch_queue_t)sizingQueue
{
static dispatch_queue_t sizingQueue = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sizingQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDataController.sizingQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_set_specific(sizingQueue, kASSizingQueueContext, kASSizingQueueContext, NULL);
dispatch_set_target_queue(sizingQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
});
return sizingQueue;
}
+ (BOOL)executingOnSizingQueue
{
return kASSizingQueueContext == dispatch_get_specific(kASSizingQueueContext);
}
- (void)asyncUpdateDataWithBlock:(dispatch_block_t)block {
dispatch_async(dispatch_get_main_queue(), ^{
if (_batchUpdateCounter) {
[_pendingBlocks addObject:block];
} else {
block();
}
});
}
- (void)syncUpdateDataWithBlock:(dispatch_block_t)block {
dispatch_sync(dispatch_get_main_queue(), ^{
if (_batchUpdateCounter) {
[_pendingBlocks addObject:block];
} else {
block();
}
});
}
- (void)accessDataSourceWithBlock:(dispatch_block_t)block {
if (_asyncDataFetchingEnabled) {
[_dataSource dataControllerLockDataSource];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
block();
[_dataSource dataControllerUnlockDataSource];
});
} else {
[_dataSource dataControllerLockDataSource];
block();
[_dataSource dataControllerUnlockDataSource];
}
}
#pragma mark - Cell Layout #pragma mark - Cell Layout
- (void)_layoutNodes:(NSArray *)nodes - (void)_layoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
atIndexPaths:(NSArray *)indexPaths
withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"Cell node layout must be initiated from edit transaction queue");
if (!nodes.count) { if (!nodes.count) {
return; return;
} }
@@ -172,25 +122,14 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
}); });
} }
dispatch_block_t block = ^{ // Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated.
dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER);
[self asyncUpdateDataWithBlock:^{
// Insert finished nodes into data storage
[self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
};
if ([ASDataController executingOnSizingQueue]) { // Insert finished nodes into data storage
block(); [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
} else {
dispatch_async([ASDataController sizingQueue], block);
}
} }
- (void)_batchLayoutNodes:(NSArray *)nodes - (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
atIndexPaths:(NSArray *)indexPaths
withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor;
@@ -204,132 +143,187 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
} }
} }
#pragma mark - Internal Data Editing #pragma mark - Internal Data Querying + Editing
- (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)_insertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
if (indexPaths.count == 0) if (indexPaths.count == 0)
return; return;
if (_delegateWillInsertNodes) ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths, nodes);
[_delegate dataController:self willInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_nodes, indexPaths, nodes); // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads.
if (_delegateDidInsertNodes) NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes);
[_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
ASDisplayNodePerformBlockOnMainThread(^{
_completedNodes = completedNodes;
if (_delegateDidInsertNodes)
[_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
});
} }
- (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
if (indexPaths.count == 0) if (indexPaths.count == 0)
return; return;
if (_delegateWillDeleteNodes) ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths);
[_delegate dataController:self willDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); ASDisplayNodePerformBlockOnMainThread(^{
if (_delegateDidDeleteNodes) ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths);
[_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; if (_delegateDidDeleteNodes)
[_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
});
} }
- (void)_insertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)_insertSections:(NSMutableArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
if (indexSet.count == 0) if (indexSet.count == 0)
return; return;
if (_delegateWillInsertSections) [_editingNodes insertObjects:sections atIndexes:indexSet];
[_delegate dataController:self willInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions];
[_nodes insertObjects:sections atIndexes:indexSet]; // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads.
if (_delegateDidInsertSections) NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections);
[_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions];
ASDisplayNodePerformBlockOnMainThread(^{
[_completedNodes insertObjects:sectionsForCompleted atIndexes:indexSet];
if (_delegateDidInsertSections)
[_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions];
});
} }
- (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
if (indexSet.count == 0) if (indexSet.count == 0)
return; return;
if (_delegateWillDeleteSections) [_editingNodes removeObjectsAtIndexes:indexSet];
[_delegate dataController:self willDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; ASDisplayNodePerformBlockOnMainThread(^{
[_nodes removeObjectsAtIndexes:indexSet]; [_completedNodes removeObjectsAtIndexes:indexSet];
if (_delegateDidDeleteSections) if (_delegateDidDeleteSections)
[_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
});
} }
#pragma mark - Initial Load & Full Reload (External API) #pragma mark - Initial Load & Full Reload (External API)
- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
[self accessDataSourceWithBlock:^{ {
NSMutableArray *indexPaths = [NSMutableArray array]; [self performEditCommandWithBlock:^{
NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; ASDisplayNodeAssertMainThread();
[self accessDataSourceWithBlock:^{
NSMutableArray *indexPaths = [NSMutableArray array];
NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self];
// insert sections // insert sections
[self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0];
for (NSUInteger i = 0; i < sectionNum; i++) { for (NSUInteger i = 0; i < sectionNum; i++) {
NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i];
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i];
for (NSUInteger j = 0; j < rowNum; j++) { for (NSUInteger j = 0; j < rowNum; j++) {
[indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; [indexPaths addObject:[indexPath indexPathByAddingIndex:j]];
}
} }
}
// insert elements
[self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
// insert elements
[self insertRowsAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
}]; }];
} }
- (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^)())completion - (void)reloadDataWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions completion:(void (^)())completion
{ {
[self accessDataSourceWithBlock:^{ [self performEditCommandWithBlock:^{
// Fetching data in calling thread ASDisplayNodeAssertMainThread();
NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init];
[self accessDataSourceWithBlock:^{
NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; NSUInteger sectionCount = [_dataSource dataControllerNumberOfSections:self];
for (NSUInteger i = 0; i < sectionNum; i++) { NSMutableArray *updatedNodes = [NSMutableArray array];
NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; NSMutableArray *updatedIndexPaths = [NSMutableArray array];
[self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; [_editingTransactionQueue addOperationWithBlock:^{
for (NSUInteger j = 0; j < rowNum; j++) {
NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j];
[updatedIndexPaths addObject:indexPath];
[updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]];
}
}
dispatch_async([ASDataController sizingQueue], ^{
[self syncUpdateDataWithBlock:^{
// Remove everything that existed before the reload, now that we're ready to insert replacements // Remove everything that existed before the reload, now that we're ready to insert replacements
NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_nodes); NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _nodes.count)]; NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _editingNodes.count)];
[self deleteSections:indexSet withAnimationOptions:animationOptions]; [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
// Insert each section // Insert each section
NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:sectionNum]; NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];
for (int i = 0; i < sectionNum; i++) { for (int i = 0; i < sectionCount; i++) {
[sections addObject:[[NSMutableArray alloc] init]]; [sections addObject:[[NSMutableArray alloc] init]];
} }
[self _insertSections:sections atIndexSet:[[NSIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:animationOptions]; [self _insertSections:sections atIndexSet:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)] withAnimationOptions:animationOptions];
[self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions];
if (completion) {
dispatch_async(dispatch_get_main_queue(), completion);
}
}]; }];
}];
[self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions];
if (completion) {
dispatch_async(dispatch_get_main_queue(), completion);
}
});
}]; }];
} }
#pragma mark - Data Source Access (Calling _dataSource)
- (void)accessDataSourceWithBlock:(dispatch_block_t)block
{
if (_asyncDataFetchingEnabled) {
[_dataSource dataControllerLockDataSource];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
block();
[_dataSource dataControllerUnlockDataSource];
});
} else {
[_dataSource dataControllerLockDataSource];
block();
[_dataSource dataControllerUnlockDataSource];
}
}
- (void)_populateFromDataSourceWithSectionIndexSet:(NSIndexSet *)indexSet mutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths
{
[indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx];
NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx];
for (NSUInteger i = 0; i < rowNum; i++) {
NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i];
[indexPaths addObject:indexPath];
[nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]];
}
}];
}
- (void)_populateFromEntireDataSourceWithMutableNodes:(NSMutableArray *)nodes mutableIndexPaths:(NSMutableArray *)indexPaths
{
NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self];
for (NSUInteger i = 0; i < sectionNum; i++) {
NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i];
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i];
for (NSUInteger j = 0; j < rowNum; j++) {
NSIndexPath *indexPath = [sectionIndexPath indexPathByAddingIndex:j];
[indexPaths addObject:indexPath];
[nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]];
}
}
}
#pragma mark - Batching (External API) #pragma mark - Batching (External API)
- (void)beginUpdates - (void)beginUpdates
{ {
dispatch_async([[self class] sizingQueue], ^{ // Begin queuing up edit calls that happen on the main thread.
[self asyncUpdateDataWithBlock:^{ // This will prevent further operations from being scheduled on _editingTransactionQueue.
_batchUpdateCounter++; // It's fine if there is an in-flight operation on _editingTransactionQueue,
}]; // as once the command queue is unpaused, each edit command will wait for the _editingTransactionQueue to be flushed.
}); _batchUpdateCounter++;
} }
- (void)endUpdates - (void)endUpdates
@@ -339,119 +333,110 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion - (void)endUpdatesWithCompletion:(void (^)(BOOL))completion
{ {
dispatch_async([[self class] sizingQueue], ^{ _batchUpdateCounter--;
dispatch_async(dispatch_get_main_queue(), ^{
_batchUpdateCounter--;
if (!_batchUpdateCounter) { if (_batchUpdateCounter == 0) {
[_delegate dataControllerBeginUpdates:self]; [_delegate dataControllerBeginUpdates:self];
[_pendingBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates.
block(); // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction.
}]; [_pendingEditCommandBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) {
[_pendingBlocks removeAllObjects]; block();
[_delegate dataControllerEndUpdates:self completion:completion]; }];
} [_pendingEditCommandBlocks removeAllObjects];
});
}); [_delegate dataControllerEndUpdates:self completion:completion];
}
}
- (void)performEditCommandWithBlock:(void (^)(void))block
{
// This method needs to block the thread and synchronously perform the operation if we are not
// queuing commands for begin/endUpdates. If we are queuing, it needs to return immediately.
if (_batchUpdateCounter == 0) {
block();
} else {
[_pendingEditCommandBlocks addObject:block];
}
} }
#pragma mark - Section Editing (External API) #pragma mark - Section Editing (External API)
- (void)insertSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)insertSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
[self accessDataSourceWithBlock:^{ [self performEditCommandWithBlock:^{
__block int nodeTotalCnt = 0; ASDisplayNodeAssertMainThread();
NSMutableArray *nodeCounts = [NSMutableArray arrayWithCapacity:indexSet.count]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSUInteger cnt = [_dataSource dataController:self rowsInSection:idx]; [self accessDataSourceWithBlock:^{
[nodeCounts addObject:@(cnt)]; NSMutableArray *updatedNodes = [NSMutableArray array];
nodeTotalCnt += cnt; NSMutableArray *updatedIndexPaths = [NSMutableArray array];
}]; [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];
NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:nodeTotalCnt]; [_editingTransactionQueue addOperationWithBlock:^{
NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodeTotalCnt];
__block NSUInteger idx = 0;
[indexSet enumerateIndexesUsingBlock:^(NSUInteger sectionIdx, BOOL *stop) {
NSUInteger cnt = [nodeCounts[idx++] unsignedIntegerValue];
for (int i = 0; i < cnt; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:sectionIdx];
[indexPaths addObject:indexPath];
ASCellNode *node = [_dataSource dataController:self nodeAtIndexPath:indexPath];
[nodes addObject:node];
}
}];
dispatch_async([[self class] sizingQueue], ^{
[self syncUpdateDataWithBlock:^{
NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count];
for (NSUInteger i = 0; i < indexSet.count; i++) { for (NSUInteger i = 0; i < indexSet.count; i++) {
[sectionArray addObject:[NSMutableArray array]]; [sectionArray addObject:[NSMutableArray array]];
} }
[self _insertSections:sectionArray atIndexSet:indexSet withAnimationOptions:animationOptions]; [self _insertSections:sectionArray atIndexSet:indexSet withAnimationOptions:animationOptions];
[self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions];
}]; }];
}];
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
});
}]; }];
} }
- (void)deleteSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)deleteSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
dispatch_async([[self class] sizingQueue], ^{ [self performEditCommandWithBlock:^{
[self asyncUpdateDataWithBlock:^{ ASDisplayNodeAssertMainThread();
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{
// remove elements // remove elements
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, indexSet); NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
[self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
}]; }];
}); }];
} }
- (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
[self accessDataSourceWithBlock:^{ [self performEditCommandWithBlock:^{
// We need to keep data query on data source in the calling thread. ASDisplayNodeAssertMainThread();
NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
NSMutableArray *updatedNodes = [[NSMutableArray alloc] init];
[sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { [self accessDataSourceWithBlock:^{
NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx]; NSMutableArray *updatedNodes = [NSMutableArray array];
NSMutableArray *updatedIndexPaths = [NSMutableArray array];
[self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];
NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; // Dispatch to sizing queue in order to guarantee that any in-progress sizing operations from prior edits have completed.
for (NSUInteger i = 0; i < rowNum; i++) { // For example, if an initial -reloadData call is quickly followed by -reloadSections, sizing the initial set may not be done
NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; // at this time. Thus _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...]
[updatedIndexPaths addObject:indexPath];
[updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; [_editingTransactionQueue addOperationWithBlock:^{
} NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections);
}];
// Dispatch to sizing queue in order to guarantee that any in-progress sizing operations from prior edits have completed.
// For example, if an initial -reloadData call is quickly followed by -reloadSections, sizing the initial set may not be done
// at this time. Thus _nodes could be empty and crash in ASIndexPathsForMultidimensional[...]
dispatch_async([ASDataController sizingQueue], ^{
[self syncUpdateDataWithBlock:^{
// remove elements
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, sections);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
// reinsert the elements
[self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions];
}]; }];
}];
// reinsert the elements
[self _batchLayoutNodes:updatedNodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions];
});
}]; }];
} }
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
dispatch_async([ASDataController sizingQueue], ^{ [self performEditCommandWithBlock:^{
[self asyncUpdateDataWithBlock:^{ ASDisplayNodeAssertMainThread();
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{
// remove elements // remove elements
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, [NSIndexSet indexSetWithIndex:section]); NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]);
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
// update the section of indexpaths // update the section of indexpaths
@@ -464,62 +449,77 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
// Don't re-calculate size for moving // Don't re-calculate size for moving
[self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions];
}]; }];
}); }];
} }
#pragma mark - Row Editing (External API) #pragma mark - Row Editing (External API)
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
[self accessDataSourceWithBlock:^{ [self performEditCommandWithBlock:^{
// sort indexPath to avoid messing up the index when inserting in several batches ASDisplayNodeAssertMainThread();
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
for (NSUInteger i = 0; i < sortedIndexPaths.count; i++) { [self accessDataSourceWithBlock:^{
[nodes addObject:[_dataSource dataController:self nodeAtIndexPath:sortedIndexPaths[i]]]; // sort indexPath to avoid messing up the index when inserting in several batches
} NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; for (NSUInteger i = 0; i < sortedIndexPaths.count; i++) {
[nodes addObject:[_dataSource dataController:self nodeAtIndexPath:sortedIndexPaths[i]]];
}
[_editingTransactionQueue addOperationWithBlock:^{
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
}];
}]; }];
} }
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
// sort indexPath in order to avoid messing up the index when deleting [self performEditCommandWithBlock:^{
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; ASDisplayNodeAssertMainThread();
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
// sort indexPath in order to avoid messing up the index when deleting
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
dispatch_async([ASDataController sizingQueue], ^{ [_editingTransactionQueue addOperationWithBlock:^{
[self asyncUpdateDataWithBlock:^{
[self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions];
}]; }];
}); }];
} }
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
// Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. [self performEditCommandWithBlock:^{
[self accessDataSourceWithBlock:^{ ASDisplayNodeAssertMainThread();
NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[indexPaths sortedArrayUsingSelector:@selector(compare:)];
[indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source.
[nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; [self accessDataSourceWithBlock:^{
}]; NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count];
[indexPaths sortedArrayUsingSelector:@selector(compare:)];
dispatch_async([ASDataController sizingQueue], ^{ [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
[self syncUpdateDataWithBlock:^{ [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]];
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
}]; }];
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; [_editingTransactionQueue addOperationWithBlock:^{
}); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
}];
}]; }];
} }
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
dispatch_async([ASDataController sizingQueue], ^{ [self performEditCommandWithBlock:^{
[self asyncUpdateDataWithBlock:^{ ASDisplayNodeAssertMainThread();
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [NSArray arrayWithObject:indexPath]); [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]);
NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -527,7 +527,7 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath]; NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath];
[self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions];
}]; }];
}); }];
} }
#pragma mark - Data Querying (External API) #pragma mark - Data Querying (External API)
@@ -535,19 +535,19 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
- (NSUInteger)numberOfSections - (NSUInteger)numberOfSections
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
return [_nodes count]; return [_completedNodes count];
} }
- (NSUInteger)numberOfRowsInSection:(NSUInteger)section - (NSUInteger)numberOfRowsInSection:(NSUInteger)section
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
return [_nodes[section] count]; return [_completedNodes[section] count];
} }
- (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath - (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
return _nodes[indexPath.section][indexPath.row]; return _completedNodes[indexPath.section][indexPath.row];
} }
- (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths
@@ -557,19 +557,19 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
// Make sure that any asynchronous layout operations have finished so that those nodes are present. // Make sure that any asynchronous layout operations have finished so that those nodes are present.
// Otherwise a failure case could be: // Otherwise a failure case could be:
// - Reload section 2, deleting all current nodes in that section. // - Reload section 2, deleting all current nodes in that section.
// - New nodes are created and sizing is triggered, but they are not yet added to _nodes. // - New nodes are created and sizing is triggered, but they are not yet added to _completedNodes.
// - This method is called and includes an indexPath in section 2. // - This method is called and includes an indexPath in section 2.
// - Unless we wait for the layout group to finish, we will crash with array out of bounds looking for the index in _nodes. // - Unless we wait for the layout group to finish, we will crash with array out of bounds looking for the index in _completedNodes.
// FIXME: Seralization is required here. Diff in progress to resolve.
return ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]);
return ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]);
} }
#pragma mark - Dealloc #pragma mark - Dealloc
- (void)dealloc { - (void)dealloc
{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
[_nodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) { [_completedNodes enumerateObjectsUsingBlock:^(NSMutableArray *section, NSUInteger sectionIndex, BOOL *stop) {
[section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) { [section enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger rowIndex, BOOL *stop) {
if (node.isNodeLoaded) { if (node.isNodeLoaded) {
if (node.layerBacked) { if (node.layerBacked) {

View File

@@ -77,8 +77,7 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;
} }
- (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet { - (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet {
[indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) [indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) {
{
_nodeSizes.erase(_nodeSizes.begin() +idx); _nodeSizes.erase(_nodeSizes.begin() +idx);
}]; }];
} }
@@ -148,6 +147,7 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;
[indexPathSet addObject:[NSIndexPath indexPathForRow:startIter.second inSection:startIter.first]]; [indexPathSet addObject:[NSIndexPath indexPathForRow:startIter.second inSection:startIter.first]];
startIter.second++; startIter.second++;
// Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized.
while (startIter.second == _nodeSizes[startIter.first].size() && startIter.first < _nodeSizes.size()) { while (startIter.second == _nodeSizes[startIter.first].size() && startIter.first < _nodeSizes.size()) {
startIter.second = 0; startIter.second = 0;
startIter.first++; startIter.first++;

View File

@@ -44,7 +44,7 @@ extern NSArray *ASFindElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray
extern NSArray *ASIndexPathsForMultidimensionalArrayAtIndexSet(NSArray *MultidimensionalArray, NSIndexSet *indexSet); extern NSArray *ASIndexPathsForMultidimensionalArrayAtIndexSet(NSArray *MultidimensionalArray, NSIndexSet *indexSet);
/** /**
* Reteurn all the index paths of mutable multidimensional array, in ascending order. * Return all the index paths of mutable multidimensional array, in ascending order.
*/ */
extern NSArray *ASIndexPathsForMultidimensionalArray(NSArray *MultidimensionalArray); extern NSArray *ASIndexPathsForMultidimensionalArray(NSArray *MultidimensionalArray);

View File

@@ -107,26 +107,4 @@
*/ */
- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
@optional
/**
* Called before nodes insertion.
*/
- (void)rangeController:(ASRangeController *)rangeController willInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/**
* Called before nodes deletion.
*/
- (void)rangeController:(ASRangeController *)rangeController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/**
* Called before section insertion.
*/
- (void)rangeController:(ASRangeController *)rangeController willInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/**
* Called before section deletion.
*/
- (void)rangeController:(ASRangeController *)rangeController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
@end @end

View File

@@ -111,6 +111,7 @@
NSMutableSet *removedIndexPaths = _rangeIsValid ? [[_rangeTypeIndexPaths objectForKey:rangeKey] mutableCopy] : [NSMutableSet set]; NSMutableSet *removedIndexPaths = _rangeIsValid ? [[_rangeTypeIndexPaths objectForKey:rangeKey] mutableCopy] : [NSMutableSet set];
[removedIndexPaths minusSet:indexPaths]; [removedIndexPaths minusSet:indexPaths];
[removedIndexPaths minusSet:visibleNodePathsSet]; [removedIndexPaths minusSet:visibleNodePathsSet];
if (removedIndexPaths.count) { if (removedIndexPaths.count) {
NSArray *removedNodes = [_delegate rangeController:self nodesAtIndexPaths:[removedIndexPaths allObjects]]; NSArray *removedNodes = [_delegate rangeController:self nodesAtIndexPaths:[removedIndexPaths allObjects]];
[removedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { [removedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) {
@@ -129,7 +130,7 @@
if ([self shouldSkipVisibleNodesForRangeType:rangeType]) { if ([self shouldSkipVisibleNodesForRangeType:rangeType]) {
[addedIndexPaths minusSet:visibleNodePathsSet]; [addedIndexPaths minusSet:visibleNodePathsSet];
} }
if (addedIndexPaths.count) { if (addedIndexPaths.count) {
NSArray *addedNodes = [_delegate rangeController:self nodesAtIndexPaths:[addedIndexPaths allObjects]]; NSArray *addedNodes = [_delegate rangeController:self nodesAtIndexPaths:[addedIndexPaths allObjects]];
[addedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { [addedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) {
@@ -181,14 +182,6 @@
}); });
} }
- (void)dataController:(ASDataController *)dataController willInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{
if ([_delegate respondsToSelector:@selector(rangeController:willInsertNodesAtIndexPaths:withAnimationOptions:)]) {
[_delegate rangeController:self willInsertNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
}
});
}
- (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path");
@@ -206,14 +199,6 @@
}); });
} }
- (void)dataController:(ASDataController *)dataController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{
if ([_delegate respondsToSelector:@selector(rangeController:willDeleteNodesAtIndexPaths:withAnimationOptions:)]) {
[_delegate rangeController:self willDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
}
});
}
- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
if ([_layoutController respondsToSelector:@selector(deleteNodesAtIndexPaths:)]) { if ([_layoutController respondsToSelector:@selector(deleteNodesAtIndexPaths:)]) {
@@ -224,14 +209,6 @@
}); });
} }
- (void)dataController:(ASDataController *)dataController willInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{
if ([_delegate respondsToSelector:@selector(rangeController:willInsertSectionsAtIndexSet:withAnimationOptions:)]) {
[_delegate rangeController:self willInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
}
});
}
- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections"); ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections");
@@ -254,14 +231,6 @@
}); });
} }
- (void)dataController:(ASDataController *)dataController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{
if ([_delegate respondsToSelector:@selector(rangeController:willDeleteSectionsAtIndexSet:withAnimationOptions:)]) {
[_delegate rangeController:self willDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions];
}
});
}
- (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
if ([_layoutController respondsToSelector:@selector(deleteSectionsAtIndexSet:)]) { if ([_layoutController respondsToSelector:@selector(deleteSectionsAtIndexSet:)]) {

View File

@@ -113,6 +113,33 @@
XCTAssertTrue(tableViewDidDealloc, @"unexpected table view lifetime:%@", tableView); XCTAssertTrue(tableViewDidDealloc, @"unexpected table view lifetime:%@", tableView);
} }
- (NSIndexSet *)randomIndexSet
{
NSInteger randA = arc4random_uniform(NumberOfSections - 1);
NSInteger randB = arc4random_uniform(NumberOfSections - 1);
return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))];
}
- (NSArray *)randomIndexPathsExisting:(BOOL)existing
{
NSMutableArray *indexPaths = [NSMutableArray array];
[[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSUInteger rowNum = NumberOfRowsPerSection;
NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx];
for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) {
// Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows
if (existing && arc4random_uniform(2) == 0) {
continue;
}
NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i];
[indexPaths addObject:indexPath];
}
}];
return indexPaths;
}
- (void)testReloadData - (void)testReloadData
{ {
// Keep the viewport moderately sized so that new cells are loaded on scrolling // Keep the viewport moderately sized so that new cells are loaded on scrolling
@@ -127,22 +154,34 @@
[tableView reloadData]; [tableView reloadData];
[tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1,2)] withRowAnimation:UITableViewRowAnimationNone];
// FIXME: Early return because we can't currently pass this test :). Diff is in progress to resolve.
return;
for (int i = 0; i < NumberOfReloadIterations; ++i) { for (int i = 0; i < NumberOfReloadIterations; ++i) {
NSInteger randA = arc4random_uniform(NumberOfSections - 1); UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone);
NSInteger randB = arc4random_uniform(NumberOfSections - 1);
[tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))] withRowAnimation:UITableViewRowAnimationNone]; BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO);
if (useBeginEndUpdates) {
[tableView beginUpdates];
}
BOOL animated = (arc4random_uniform(1) == 0 ? YES : NO); if (reloadRowsInsteadOfSections) {
[tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation];
} else {
[tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation];
}
[tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animated]; [tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animatedScroll];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; if (letRunloopProceed) {
// Run other stuff on the main queue for between 2ms and 1000ms.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]];
}
if (useBeginEndUpdates) {
[tableView endUpdates];
}
} }
} }

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "0620" LastUpgradeVersion = "0630"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@@ -16,11 +16,12 @@
#define NumberOfSections 10 #define NumberOfSections 10
#define NumberOfRowsPerSection 20 #define NumberOfRowsPerSection 20
#define NumberOfReloadIterations 50 #define NumberOfReloadIterations 500
@interface ViewController () <ASTableViewDataSource, ASTableViewDelegate> @interface ViewController () <ASTableViewDataSource, ASTableViewDelegate>
{ {
ASTableView *_tableView; ASTableView *_tableView;
NSMutableArray *_sections; // Contains arrays of indexPaths representing rows
} }
@end @end
@@ -28,18 +29,24 @@
@implementation ViewController @implementation ViewController
#pragma mark -
#pragma mark UIViewController.
- (instancetype)init - (instancetype)init
{ {
if (!(self = [super init])) if (!(self = [super init]))
return nil; return nil;
_tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain asyncDataFetching:YES]; _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain asyncDataFetching:YES];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator
_tableView.asyncDataSource = self; _tableView.asyncDataSource = self;
_tableView.asyncDelegate = self; _tableView.asyncDelegate = self;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_sections = [NSMutableArray arrayWithCapacity:NumberOfSections];
for (int i = 0; i < NumberOfSections; i++) {
NSMutableArray *rowsArray = [NSMutableArray arrayWithCapacity:NumberOfRowsPerSection];
for (int j = 0; j < NumberOfRowsPerSection; j++) {
[rowsArray addObject:[NSIndexPath indexPathForRow:j inSection:i]];
}
[_sections addObject:rowsArray];
}
return self; return self;
} }
@@ -63,42 +70,99 @@
[self thrashTableView]; [self thrashTableView];
} }
- (NSIndexSet *)randomIndexSet
{
u_int32_t upperBound = (u_int32_t)_sections.count - 1;
u_int32_t randA = arc4random_uniform(upperBound);
u_int32_t randB = arc4random_uniform(upperBound);
return [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))];
}
- (NSArray *)randomIndexPathsExisting:(BOOL)existing
{
NSMutableArray *indexPaths = [NSMutableArray array];
[[self randomIndexSet] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSUInteger rowNum = [self tableView:_tableView numberOfRowsInSection:idx];
NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx];
for (NSUInteger i = (existing ? 0 : rowNum); i < (existing ? rowNum : rowNum * 2); i++) {
// Maximize evility by sporadically skipping indicies 1/3rd of the time, but only if reloading existing rows
if (existing && arc4random_uniform(2) == 0) {
continue;
}
NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i];
[indexPaths addObject:indexPath];
}
}];
return indexPaths;
}
- (void)thrashTableView - (void)thrashTableView
{ {
// Keep the viewport moderately sized so that new cells are loaded on scrolling _tableView.asyncDelegate = self;
ASTableView *tableView = [[ASTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 500) _tableView.asyncDataSource = self;
style:UITableViewStylePlain
asyncDataFetching:NO];
tableView.asyncDelegate = self; [_tableView reloadData];
tableView.asyncDataSource = self;
[tableView reloadData];
[tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1,2)] withRowAnimation:UITableViewRowAnimationNone];
NSArray *indexPathsAddedAndRemoved = nil;
for (int i = 0; i < NumberOfReloadIterations; ++i) { for (int i = 0; i < NumberOfReloadIterations; ++i) {
NSInteger randA = arc4random_uniform(NumberOfSections - 1); UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone);
NSInteger randB = arc4random_uniform(NumberOfSections - 1);
BOOL animatedScroll = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL reloadRowsInsteadOfSections = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL letRunloopProceed = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL addIndexPaths = (arc4random_uniform(1) == 0 ? YES : NO);
BOOL useBeginEndUpdates = (arc4random_uniform(2) == 0 ? YES : NO);
[tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(MIN(randA, randB), MAX(randA, randB) - MIN(randA, randB))] withRowAnimation:UITableViewRowAnimationNone]; if (useBeginEndUpdates) {
[_tableView beginUpdates];
}
BOOL animated = (arc4random_uniform(1) == 0 ? YES : NO); if (reloadRowsInsteadOfSections) {
[_tableView reloadRowsAtIndexPaths:[self randomIndexPathsExisting:YES] withRowAnimation:rowAnimation];
} else {
[_tableView reloadSections:[self randomIndexSet] withRowAnimation:rowAnimation];
}
[tableView setContentOffset:CGPointMake(0, arc4random_uniform(tableView.contentSize.height - tableView.bounds.size.height)) animated:animated]; if (addIndexPaths && !indexPathsAddedAndRemoved) {
indexPathsAddedAndRemoved = [self randomIndexPathsExisting:NO];
for (NSIndexPath *indexPath in indexPathsAddedAndRemoved) {
[_sections[indexPath.section] addObject:indexPath];
}
[_tableView insertRowsAtIndexPaths:indexPathsAddedAndRemoved withRowAnimation:rowAnimation];
}
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; [_tableView setContentOffset:CGPointMake(0, arc4random_uniform(_tableView.contentSize.height - _tableView.bounds.size.height)) animated:animatedScroll];
if (letRunloopProceed) {
// Run other stuff on the main queue for between 2ms and 1000ms.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:(1 / (1 + arc4random_uniform(500)))]];
if (indexPathsAddedAndRemoved) {
for (NSIndexPath *indexPath in indexPathsAddedAndRemoved) {
[_sections[indexPath.section] removeObjectIdenticalTo:indexPath];
}
[_tableView deleteRowsAtIndexPaths:indexPathsAddedAndRemoved withRowAnimation:rowAnimation];
indexPathsAddedAndRemoved = nil;
}
}
if (useBeginEndUpdates) {
[_tableView endUpdates];
}
} }
} }
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{ {
return NumberOfSections; return _sections.count;
} }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{ {
return NumberOfRowsPerSection; return [(NSArray *)[_sections objectAtIndex:section] count];
} }
- (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath