mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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++;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:)]) {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user