diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index 7341bf3bac..a5068cf20d 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '1.2.1' + spec.version = '1.2.2' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com', 'Ryan Nystrom' => 'rnystrom@fb.com' } spec.summary = 'Smooth asynchronous user interfaces for iOS apps.' - spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.2.1' } + spec.source = { :git => 'https://github.com/facebook/AsyncDisplayKit.git', :tag => '1.2.2' } spec.documentation_url = 'http://asyncdisplaykit.org/appledoc/' diff --git a/AsyncDisplayKit/ASCellNode.m b/AsyncDisplayKit/ASCellNode.m index b75e73daee..39aadab6bb 100644 --- a/AsyncDisplayKit/ASCellNode.m +++ b/AsyncDisplayKit/ASCellNode.m @@ -26,6 +26,7 @@ // use UITableViewCell defaults _selectionStyle = UITableViewCellSelectionStyleDefault; + self.clipsToBounds = YES; return self; } diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 21c867b413..bd0c5727ab 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -433,6 +433,16 @@ typedef CALayer *(^ASDisplayNodeLayerBlock)(); */ - (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node; +/** @name UIResponder methods */ + +// By default these fall through to the underlying view, but can be overridden. +- (BOOL)canBecomeFirstResponder; // default==NO +- (BOOL)becomeFirstResponder; // default==NO (no-op) +- (BOOL)canResignFirstResponder; // default==YES +- (BOOL)resignFirstResponder; // default==NO (no-op) +- (BOOL)isFirstResponder; +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender; + @end diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 54316bd371..bf81cdc960 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1368,6 +1368,7 @@ static NSInteger incrementIfFound(NSInteger i) { { self.layer.contents = nil; _placeholderLayer.contents = nil; + _placeholderImage = nil; } - (void)recursivelyClearContents @@ -1741,6 +1742,35 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, } +- (BOOL)canBecomeFirstResponder { + return NO; +} + +- (BOOL)canResignFirstResponder { + return YES; +} + +- (BOOL)isFirstResponder { + ASDisplayNodeAssertMainThread(); + return _view != nil && [_view isFirstResponder]; +} + +// Note: this implicitly loads the view if it hasn't been loaded yet. +- (BOOL)becomeFirstResponder { + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self canBecomeFirstResponder] && [self.view becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder { + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self canResignFirstResponder] && [_view resignFirstResponder]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender { + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self.view canPerformAction:action withSender:sender]; +} + @end @implementation ASDisplayNode (Debugging) diff --git a/AsyncDisplayKit/ASEditableTextNode.h b/AsyncDisplayKit/ASEditableTextNode.h index 11cfaf8405..92811e2457 100644 --- a/AsyncDisplayKit/ASEditableTextNode.h +++ b/AsyncDisplayKit/ASEditableTextNode.h @@ -57,10 +57,10 @@ - (BOOL)isFirstResponder; //! @abstract Makes the receiver's text view the first responder. -- (void)becomeFirstResponder; +- (BOOL)becomeFirstResponder; //! @abstract Resigns the receiver's text view from first-responder status, if it has it. -- (void)resignFirstResponder; +- (BOOL)resignFirstResponder; #pragma mark - Geometry /** diff --git a/AsyncDisplayKit/ASEditableTextNode.mm b/AsyncDisplayKit/ASEditableTextNode.mm index a34afe2075..3761134481 100644 --- a/AsyncDisplayKit/ASEditableTextNode.mm +++ b/AsyncDisplayKit/ASEditableTextNode.mm @@ -352,16 +352,26 @@ return [_textKitComponents.textView isFirstResponder]; } -- (void)becomeFirstResponder -{ - ASDN::MutexLocker l(_textKitLock); - [_textKitComponents.textView becomeFirstResponder]; +- (BOOL)canBecomeFirstResponder { + ASDN::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canBecomeFirstResponder]; } -- (void)resignFirstResponder +- (BOOL)becomeFirstResponder { ASDN::MutexLocker l(_textKitLock); - [_textKitComponents.textView resignFirstResponder]; + return [_textKitComponents.textView becomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + ASDN::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canResignFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + ASDN::MutexLocker l(_textKitLock); + return [_textKitComponents.textView resignFirstResponder]; } #pragma mark - UITextView Delegate diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 2085b6f543..b5ff1fb48f 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -10,7 +10,7 @@ #import "ASAssert.h" #import "ASDataController.h" -#import "ASFlowLayoutController.h" +#import "ASCollectionViewLayoutController.h" #import "ASLayoutController.h" #import "ASRangeController.h" #import "ASDisplayNodeInternal.h" @@ -160,7 +160,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (void)configureWithAsyncDataFetching:(BOOL)asyncDataFetchingEnabled { _layoutController = [[ASFlowLayoutController alloc] initWithScrollOption:ASFlowLayoutDirectionVertical]; - + _rangeController = [[ASRangeController alloc] init]; _rangeController.layoutController = _layoutController; _rangeController.delegate = self; @@ -168,6 +168,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { _dataController = [[ASDataController alloc] initWithAsyncDataFetching:asyncDataFetchingEnabled]; _dataController.dataSource = self; _dataController.delegate = _rangeController; + + _layoutController.dataSource = _dataController; _asyncDataFetchingEnabled = asyncDataFetchingEnabled; _asyncDataSourceLocked = NO; @@ -389,6 +391,11 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { cell.backgroundColor = node.backgroundColor; cell.selectionStyle = node.selectionStyle; + // the following ensures that we clip the entire cell to it's bounds if node.clipsToBounds is set (the default) + // This is actually a workaround for a bug we are seeing in some rare cases (selected background view + // overlaps other cells if size of ASCellNode has changed.) + cell.clipsToBounds = node.clipsToBounds; + return cell; } @@ -412,20 +419,12 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { { CGPoint scrollVelocity = [self.panGestureRecognizer velocityInView:self.superview]; ASScrollDirection direction = ASScrollDirectionNone; - if (_layoutController.layoutDirection == ASFlowLayoutDirectionHorizontal) { - if (scrollVelocity.x > 0) { - direction = ASScrollDirectionRight; - } else if (scrollVelocity.x < 0) { - direction = ASScrollDirectionLeft; - } + if (scrollVelocity.y > 0) { + direction = ASScrollDirectionDown; } else { - if (scrollVelocity.y > 0) { - direction = ASScrollDirectionDown; - } else { - direction = ASScrollDirectionUp; - } + direction = ASScrollDirectionUp; } - + return direction; } diff --git a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm index 15240b3c87..8655102738 100644 --- a/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm +++ b/AsyncDisplayKit/Details/ASCollectionViewLayoutController.mm @@ -21,7 +21,8 @@ struct ASDirectionalScreenfulBuffer { typedef struct ASDirectionalScreenfulBuffer ASDirectionalScreenfulBuffer; ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDirection scrollDirection, - ASRangeTuningParameters rangeTuningParameters) { + ASRangeTuningParameters rangeTuningParameters) +{ ASDirectionalScreenfulBuffer horizontalBuffer = {0, 0}; BOOL movingRight = ASScrollDirectionContainsRight(scrollDirection); horizontalBuffer.positiveDirection = movingRight ? rangeTuningParameters.leadingBufferScreenfuls : @@ -32,7 +33,8 @@ ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferHorizontal(ASScrollDire } ASDirectionalScreenfulBuffer ASDirectionalScreenfulBufferVertical(ASScrollDirection scrollDirection, - ASRangeTuningParameters rangeTuningParameters) { + ASRangeTuningParameters rangeTuningParameters) +{ ASDirectionalScreenfulBuffer verticalBuffer = {0, 0}; BOOL movingDown = ASScrollDirectionContainsDown(scrollDirection); verticalBuffer.positiveDirection = movingDown ? rangeTuningParameters.leadingBufferScreenfuls : @@ -52,19 +54,26 @@ typedef struct ASRangeGeometry ASRangeGeometry; #pragma mark - #pragma mark ASCollectionViewLayoutController -@interface ASCollectionViewLayoutController () { - ASCollectionView * __weak _collectionView; +@interface ASCollectionViewLayoutController () +{ + UIScrollView * __weak _scrollView; + UICollectionViewLayout * __strong _collectionViewLayout; std::vector _updateRangeBoundsIndexedByRangeType; + ASScrollDirection _scrollableDirections; } @end @implementation ASCollectionViewLayoutController -- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView { +- (instancetype)initWithCollectionView:(ASCollectionView *)collectionView +{ if (!(self = [super init])) { return nil; } - _collectionView = collectionView; + + _scrollableDirections = [collectionView scrollableDirections]; + _scrollView = collectionView; + _collectionViewLayout = [collectionView collectionViewLayout]; _updateRangeBoundsIndexedByRangeType = std::vector(ASLayoutRangeTypeCount); return self; } @@ -74,22 +83,21 @@ typedef struct ASRangeGeometry ASRangeGeometry; - (NSSet *)indexPathsForScrolling:(ASScrollDirection)scrollDirection viewportSize:(CGSize)viewportSize - rangeType:(ASLayoutRangeType)rangeType { + rangeType:(ASLayoutRangeType)rangeType +{ ASRangeGeometry rangeGeometry = [self rangeGeometryWithScrollDirection:scrollDirection - collectionView:_collectionView rangeTuningParameters:[self tuningParametersForRangeType:rangeType]]; _updateRangeBoundsIndexedByRangeType[rangeType] = rangeGeometry.updateBounds; - return [self indexPathsForItemsWithinRangeBounds:rangeGeometry.rangeBounds collectionView:_collectionView]; + return [self indexPathsForItemsWithinRangeBounds:rangeGeometry.rangeBounds]; } - (ASRangeGeometry)rangeGeometryWithScrollDirection:(ASScrollDirection)scrollDirection - collectionView:(ASCollectionView *)collectionView - rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters { - CGRect rangeBounds = collectionView.bounds; - CGRect updateBounds = collectionView.bounds; - ASScrollDirection scrollableDirections = [collectionView scrollableDirections]; + rangeTuningParameters:(ASRangeTuningParameters)rangeTuningParameters +{ + CGRect rangeBounds = _scrollView.bounds; + CGRect updateBounds = _scrollView.bounds; - BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(scrollableDirections); + BOOL canScrollHorizontally = ASScrollDirectionContainsHorizontalDirection(_scrollableDirections); if (canScrollHorizontally) { ASDirectionalScreenfulBuffer horizontalBuffer = ASDirectionalScreenfulBufferHorizontal(scrollDirection, rangeTuningParameters); @@ -102,7 +110,7 @@ typedef struct ASRangeGeometry ASRangeGeometry; MIN(horizontalBuffer.positiveDirection * 0.5, 0.95)); } - BOOL canScrollVertically = ASScrollDirectionContainsVerticalDirection(scrollableDirections); + BOOL canScrollVertically = ASScrollDirectionContainsVerticalDirection(_scrollableDirections); if (canScrollVertically) { ASDirectionalScreenfulBuffer verticalBuffer = ASDirectionalScreenfulBufferVertical(scrollDirection, rangeTuningParameters); @@ -118,9 +126,10 @@ typedef struct ASRangeGeometry ASRangeGeometry; return {rangeBounds, updateBounds}; } -- (NSSet *)indexPathsForItemsWithinRangeBounds:(CGRect)rangeBounds collectionView:(ASCollectionView *)collectionView { +- (NSSet *)indexPathsForItemsWithinRangeBounds:(CGRect)rangeBounds +{ NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; - NSArray *layoutAttributes = [collectionView.collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; + NSArray *layoutAttributes = [_collectionViewLayout layoutAttributesForElementsInRect:rangeBounds]; for (UICollectionViewLayoutAttributes *la in layoutAttributes) { [indexPathSet addObject:la.indexPath]; } @@ -132,13 +141,14 @@ typedef struct ASRangeGeometry ASRangeGeometry; - (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths viewportSize:(CGSize)viewportSize - rangeType:(ASLayoutRangeType)rangeType { + rangeType:(ASLayoutRangeType)rangeType +{ CGRect updateRangeBounds = _updateRangeBoundsIndexedByRangeType[rangeType]; if (CGRectIsEmpty(updateRangeBounds)) { return YES; } - CGRect currentBounds = _collectionView.bounds; + CGRect currentBounds = _scrollView.bounds; if (CGRectIsEmpty(currentBounds)) { currentBounds = CGRectMake(0, 0, viewportSize.width, viewportSize.height); } diff --git a/AsyncDisplayKit/Details/ASDataController.h b/AsyncDisplayKit/Details/ASDataController.h index a9aa7e55ac..08dfd4fbf2 100644 --- a/AsyncDisplayKit/Details/ASDataController.h +++ b/AsyncDisplayKit/Details/ASDataController.h @@ -8,7 +8,7 @@ #import #import - +#import "ASFlowLayoutController.h" @class ASCellNode; @class ASDataController; @@ -70,25 +70,21 @@ typedef NSUInteger ASDataControllerAnimationOptions; /** 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; /** 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; /** 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; /** 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; @end @@ -101,7 +97,8 @@ typedef NSUInteger ASDataControllerAnimationOptions; * will be updated asynchronously. The dataSource must be updated to reflect the changes before these methods has been called. * For each data updatin, the corresponding methods in delegate will be called. */ -@interface ASDataController : ASDealloc2MainObject +@protocol ASFlowLayoutControllerDataSource; +@interface ASDataController : ASDealloc2MainObject /** Data source for fetching data info. @@ -117,7 +114,7 @@ typedef NSUInteger ASDataControllerAnimationOptions; * Designated iniailizer. * * @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. * 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 @@ -127,7 +124,11 @@ typedef NSUInteger ASDataControllerAnimationOptions; */ - (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; @@ -167,4 +168,6 @@ typedef NSUInteger ASDataControllerAnimationOptions; - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths; +- (NSArray *)completedNodes; // This provides efficient access to the entire _completedNodes multidimensional array. + @end diff --git a/AsyncDisplayKit/Details/ASDataController.mm b/AsyncDisplayKit/Details/ASDataController.mm index bfb56d3b67..c3d1a916c6 100644 --- a/AsyncDisplayKit/Details/ASDataController.mm +++ b/AsyncDisplayKit/Details/ASDataController.mm @@ -21,16 +21,16 @@ const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; static void *kASSizingQueueContext = &kASSizingQueueContext; @interface ASDataController () { - NSMutableArray *_nodes; - NSMutableArray *_pendingBlocks; + NSMutableArray *_completedNodes; // Main thread only. External data access can immediately query this. + 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 _delegateWillInsertNodes; BOOL _delegateDidInsertNodes; - BOOL _delegateWillDeleteNodes; BOOL _delegateDidDeleteNodes; - BOOL _delegateWillInsertSections; BOOL _delegateDidInsertSections; - BOOL _delegateWillDeleteSections; BOOL _delegateDidDeleteSections; } @@ -48,8 +48,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; return nil; } - _nodes = [NSMutableArray array]; - _pendingBlocks = [NSMutableArray array]; + _completedNodes = [NSMutableArray array]; + _editingNodes = [NSMutableArray array]; + + _pendingEditCommandBlocks = [NSMutableArray array]; + + _editingTransactionQueue = [[NSOperationQueue alloc] init]; + _editingTransactionQueue.maxConcurrentOperationCount = 1; // Serial queue + _editingTransactionQueue.name = @"org.AsyncDisplayKit.ASDataController.editingTransactionQueue"; + _batchUpdateCounter = 0; _asyncDataFetchingEnabled = asyncDataFetchingEnabled; @@ -65,18 +72,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; _delegate = delegate; // 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:)]; - _delegateWillDeleteNodes = [_delegate respondsToSelector:@selector(dataController:willDeleteNodesAtIndexPaths:withAnimationOptions:)]; _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:withAnimationOptions:)]; - _delegateWillInsertSections = [_delegate respondsToSelector:@selector(dataController:willInsertSections:atIndexSet:withAnimationOptions:)]; _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; - _delegateWillDeleteSections = [_delegate respondsToSelector:@selector(dataController:willDeleteSectionsAtIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; } -#pragma mark - Queue Management - + (NSUInteger)parallelProcessorCount { static NSUInteger parallelProcessorCount; @@ -89,64 +90,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext; 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 -- (void)_layoutNodes:(NSArray *)nodes - atIndexPaths:(NSArray *)indexPaths -withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)_layoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { + ASDisplayNodeAssert([NSOperationQueue currentQueue] == _editingTransactionQueue, @"Cell node layout must be initiated from edit transaction queue"); + if (!nodes.count) { return; } @@ -172,25 +121,14 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions }); } - dispatch_block_t block = ^{ - dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); - - [self asyncUpdateDataWithBlock:^{ - // Insert finished nodes into data storage - [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - }]; - }; + // Block the _editingTransactionQueue from executing a new edit transaction until layout is done & _editingNodes array is updated. + dispatch_group_wait(layoutGroup, DISPATCH_TIME_FOREVER); - if ([ASDataController executingOnSizingQueue]) { - block(); - } else { - dispatch_async([ASDataController sizingQueue], block); - } + // Insert finished nodes into data storage + [self _insertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; } -- (void)_batchLayoutNodes:(NSArray *)nodes - atIndexPaths:(NSArray *)indexPaths - withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +- (void)_batchLayoutNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { NSUInteger blockSize = [[ASDataController class] parallelProcessorCount] * kASDataControllerSizingCountPerProcessor; @@ -204,132 +142,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 { if (indexPaths.count == 0) return; - if (_delegateWillInsertNodes) - [_delegate dataController:self willInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; - ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_nodes, indexPaths, nodes); - if (_delegateDidInsertNodes) - [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + ASInsertElementsIntoMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths, nodes); + + // Deep copy is critical here, or future edits to the sub-arrays will pollute state between _editing and _complete on different threads. + NSMutableArray *completedNodes = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(_editingNodes); + + ASDisplayNodePerformBlockOnMainThread(^{ + _completedNodes = completedNodes; + if (_delegateDidInsertNodes) + [_delegate dataController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + }); } - (void)_deleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { if (indexPaths.count == 0) return; - if (_delegateWillDeleteNodes) - [_delegate dataController:self willDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; - ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); - if (_delegateDidDeleteNodes) - [_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); + + ASDisplayNodePerformBlockOnMainThread(^{ + ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); + 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) return; - if (_delegateWillInsertSections) - [_delegate dataController:self willInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; - [_nodes insertObjects:sections atIndexes:indexSet]; - if (_delegateDidInsertSections) - [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + [_editingNodes 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. + NSArray *sectionsForCompleted = (NSMutableArray *)ASMultidimensionalArrayDeepMutableCopy(sections); + + ASDisplayNodePerformBlockOnMainThread(^{ + [_completedNodes insertObjects:sectionsForCompleted atIndexes:indexSet]; + if (_delegateDidInsertSections) + [_delegate dataController:self didInsertSections:sections atIndexSet:indexSet withAnimationOptions:animationOptions]; + }); } - (void)_deleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { if (indexSet.count == 0) return; - if (_delegateWillDeleteSections) - [_delegate dataController:self willDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; - [_nodes removeObjectsAtIndexes:indexSet]; - if (_delegateDidDeleteSections) - [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + [_editingNodes removeObjectsAtIndexes:indexSet]; + ASDisplayNodePerformBlockOnMainThread(^{ + [_completedNodes removeObjectsAtIndexes:indexSet]; + if (_delegateDidDeleteSections) + [_delegate dataController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; + }); } #pragma mark - Initial Load & Full Reload (External API) -- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - NSMutableArray *indexPaths = [NSMutableArray array]; - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; +- (void)initialDataLoadingWithAnimationOptions:(ASDataControllerAnimationOptions)animationOptions +{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [self accessDataSourceWithBlock:^{ + NSMutableArray *indexPaths = [NSMutableArray array]; + NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; - // insert sections - [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; + // insert sections + [self insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionNum)] withAnimationOptions:0]; - for (NSUInteger i = 0; i < sectionNum; i++) { - NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; + for (NSUInteger i = 0; i < sectionNum; i++) { + NSIndexPath *indexPath = [[NSIndexPath alloc] initWithIndex:i]; - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; - for (NSUInteger j = 0; j < rowNum; j++) { - [indexPaths addObject:[indexPath indexPathByAddingIndex:j]]; + NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; + for (NSUInteger j = 0; j < rowNum; 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 { - [self accessDataSourceWithBlock:^{ - // Fetching data in calling thread - NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; - - NSUInteger sectionNum = [_dataSource dataControllerNumberOfSections:self]; - for (NSUInteger i = 0; i < sectionNum; i++) { - NSIndexPath *sectionIndexPath = [[NSIndexPath alloc] initWithIndex:i]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [self accessDataSourceWithBlock:^{ + NSUInteger sectionCount = [_dataSource dataControllerNumberOfSections:self]; + NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedIndexPaths = [NSMutableArray array]; + [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:i]; - 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:^{ + [_editingTransactionQueue addOperationWithBlock:^{ // 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]; - NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _nodes.count)]; - [self deleteSections:indexSet withAnimationOptions:animationOptions]; + NSMutableIndexSet *indexSet = [[NSMutableIndexSet alloc] initWithIndexesInRange:NSMakeRange(0, _editingNodes.count)]; + [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; // Insert each section - NSMutableArray *sections = [[NSMutableArray alloc] initWithCapacity:sectionNum]; - for (int i = 0; i < sectionNum; i++) { + NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount]; + for (int i = 0; i < sectionCount; i++) { [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) - (void)beginUpdates { - dispatch_async([[self class] sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ - _batchUpdateCounter++; - }]; - }); + // Begin queuing up edit calls that happen on the main thread. + // This will prevent further operations from being scheduled on _editingTransactionQueue. + // 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 @@ -339,119 +332,110 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)endUpdatesWithCompletion:(void (^)(BOOL))completion { - dispatch_async([[self class] sizingQueue], ^{ - dispatch_async(dispatch_get_main_queue(), ^{ - _batchUpdateCounter--; + _batchUpdateCounter--; - if (!_batchUpdateCounter) { - [_delegate dataControllerBeginUpdates:self]; - [_pendingBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { - block(); - }]; - [_pendingBlocks removeAllObjects]; - [_delegate dataControllerEndUpdates:self completion:completion]; - } - }); - }); + if (_batchUpdateCounter == 0) { + [_delegate dataControllerBeginUpdates:self]; + // Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. + // 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) { + block(); + }]; + [_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) - (void)insertSections:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - __block int nodeTotalCnt = 0; - NSMutableArray *nodeCounts = [NSMutableArray arrayWithCapacity:indexSet.count]; - [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSUInteger cnt = [_dataSource dataController:self rowsInSection:idx]; - [nodeCounts addObject:@(cnt)]; - nodeTotalCnt += cnt; - }]; - - NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:nodeTotalCnt]; - 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:^{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [self accessDataSourceWithBlock:^{ + NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedIndexPaths = [NSMutableArray array]; + [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; + + [_editingTransactionQueue addOperationWithBlock:^{ NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; for (NSUInteger i = 0; i < indexSet.count; i++) { [sectionArray addObject:[NSMutableArray array]]; } + [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 { - dispatch_async([[self class] sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [_editingTransactionQueue addOperationWithBlock:^{ // remove elements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, indexSet); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }]; - }); + }]; } - (void)reloadSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - // We need to keep data query on data source in the calling thread. - NSMutableArray *updatedIndexPaths = [[NSMutableArray alloc] init]; - NSMutableArray *updatedNodes = [[NSMutableArray alloc] init]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; - [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSUInteger rowNum = [_dataSource dataController:self rowsInSection:idx]; + [self accessDataSourceWithBlock:^{ + NSMutableArray *updatedNodes = [NSMutableArray array]; + NSMutableArray *updatedIndexPaths = [NSMutableArray array]; + [self _populateFromDataSourceWithSectionIndexSet:sections mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; - NSIndexPath *sectionIndex = [[NSIndexPath alloc] initWithIndex:idx]; - for (NSUInteger i = 0; i < rowNum; i++) { - NSIndexPath *indexPath = [sectionIndex indexPathByAddingIndex:i]; - [updatedIndexPaths addObject:indexPath]; - [updatedNodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; - } - }]; - - // 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); + // 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 _editingNodes could be empty and crash in ASIndexPathsForMultidimensional[...] + + [_editingTransactionQueue addOperationWithBlock:^{ + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); [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 { - dispatch_async([ASDataController sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [_editingTransactionQueue addOperationWithBlock:^{ // remove elements - NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_nodes, [NSIndexSet indexSetWithIndex:section]); - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, indexPaths); + NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]); + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; // update the section of indexpaths @@ -464,62 +448,77 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions // Don't re-calculate size for moving [self _insertNodes:nodes atIndexPaths:updatedIndexPaths withAnimationOptions:animationOptions]; }]; - }); + }]; } #pragma mark - Row Editing (External API) - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - [self accessDataSourceWithBlock:^{ - // 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]; - for (NSUInteger i = 0; i < sortedIndexPaths.count; i++) { - [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:sortedIndexPaths[i]]]; - } - - [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [self accessDataSourceWithBlock:^{ + // 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]; + 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 { - // sort indexPath in order to avoid messing up the index when deleting - NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + [self performEditCommandWithBlock:^{ + 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], ^{ - [self asyncUpdateDataWithBlock:^{ + [_editingTransactionQueue addOperationWithBlock:^{ [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions: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 accessDataSourceWithBlock:^{ - NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; - [indexPaths sortedArrayUsingSelector:@selector(compare:)]; - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; - }]; - - dispatch_async([ASDataController sizingQueue], ^{ - [self syncUpdateDataWithBlock:^{ - [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. + [self accessDataSourceWithBlock:^{ + NSMutableArray *nodes = [[NSMutableArray alloc] initWithCapacity:indexPaths.count]; + [indexPaths sortedArrayUsingSelector:@selector(compare:)]; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { + [nodes addObject:[_dataSource dataController:self nodeAtIndexPath:indexPath]]; }]; - - [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 { - dispatch_async([ASDataController sizingQueue], ^{ - [self asyncUpdateDataWithBlock:^{ - NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [NSArray arrayWithObject:indexPath]); + [self performEditCommandWithBlock:^{ + ASDisplayNodeAssertMainThread(); + [_editingTransactionQueue waitUntilAllOperationsAreFinished]; + + [_editingTransactionQueue addOperationWithBlock:^{ + NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]); NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; @@ -527,7 +526,7 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions NSArray *newIndexPaths = [NSArray arrayWithObject:newIndexPath]; [self _insertNodes:nodes atIndexPaths:newIndexPaths withAnimationOptions:animationOptions]; }]; - }); + }]; } #pragma mark - Data Querying (External API) @@ -535,41 +534,39 @@ withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (NSUInteger)numberOfSections { ASDisplayNodeAssertMainThread(); - return [_nodes count]; + return [_completedNodes count]; } - (NSUInteger)numberOfRowsInSection:(NSUInteger)section { ASDisplayNodeAssertMainThread(); - return [_nodes[section] count]; + return [_completedNodes[section] count]; } - (ASCellNode *)nodeAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssertMainThread(); - return _nodes[indexPath.section][indexPath.row]; + return _completedNodes[indexPath.section][indexPath.row]; } - (NSArray *)nodesAtIndexPaths:(NSArray *)indexPaths { ASDisplayNodeAssertMainThread(); - - // Make sure that any asynchronous layout operations have finished so that those nodes are present. - // Otherwise a failure case could be: - // - 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. - // - 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. - // FIXME: Seralization is required here. Diff in progress to resolve. - - return ASFindElementsInMultidimensionalArrayAtIndexPaths(_nodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]); + return ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, [indexPaths sortedArrayUsingSelector:@selector(compare:)]); +} + +- (NSArray *)completedNodes +{ + ASDisplayNodeAssertMainThread(); + return _completedNodes; } #pragma mark - Dealloc -- (void)dealloc { +- (void)dealloc +{ 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) { if (node.isNodeLoaded) { if (node.layerBacked) { diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.h b/AsyncDisplayKit/Details/ASFlowLayoutController.h index 0c682594cb..9b61a37b7d 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.h +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.h @@ -15,12 +15,20 @@ typedef NS_ENUM(NSUInteger, ASFlowLayoutDirection) { ASFlowLayoutDirectionHorizontal, }; +@protocol ASFlowLayoutControllerDataSource + +- (NSArray *)completedNodes; // This provides access to ASDataController's _completedNodes multidimensional array. + +@end + /** - * The controller for flow layout. + * An optimized flow layout controller that supports only vertical or horizontal scrolling, not simultaneously two-dimensional scrolling. + * It is used for all ASTableViews, and may be used with ASCollectionView. */ @interface ASFlowLayoutController : ASAbstractLayoutController @property (nonatomic, readonly, assign) ASFlowLayoutDirection layoutDirection; +@property (nonatomic, readwrite, weak) id dataSource; - (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection; diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm index bb5370d3bb..52b9e6e880 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.mm +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -7,111 +7,66 @@ */ #import "ASFlowLayoutController.h" +#import "ASAssert.h" +#import "ASDisplayNode.h" +#import "ASIndexPath.h" #include #include #include -#import "ASAssert.h" - static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; -@interface ASFlowLayoutController() { - std::vector > _nodeSizes; - - std::pair _visibleRangeStartPos; - std::pair _visibleRangeEndPos; - - std::vector> _rangeStartPos; - std::vector> _rangeEndPos; +@interface ASFlowLayoutController() +{ + ASIndexPathRange _visibleRange; + std::vector _rangesByType; // All ASLayoutRangeTypes besides visible. } @end @implementation ASFlowLayoutController -- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection { +- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection +{ if (!(self = [super init])) { return nil; } - _layoutDirection = layoutDirection; - + _rangesByType = std::vector(ASLayoutRangeTypeCount); return self; } -#pragma mark - Editing - -- (void)insertNodesAtIndexPaths:(NSArray *)indexPaths withSizes:(NSArray *)nodeSizes -{ - ASDisplayNodeAssert(indexPaths.count == nodeSizes.count, @"Inconsistent index paths and node size"); - - [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - std::vector &v = _nodeSizes[indexPath.section]; - v.insert(v.begin() + indexPath.row, [(NSValue *)nodeSizes[idx] CGSizeValue]); - }]; -} - -- (void)deleteNodesAtIndexPaths:(NSArray *)indexPaths -{ - [indexPaths enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - std::vector &v = _nodeSizes[indexPath.section]; - v.erase(v.begin() + indexPath.row); - }]; -} - -- (void)insertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet -{ - __block int cnt = 0; - [indexSet enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { - NSArray *nodes = sections[cnt++]; - std::vector v; - v.reserve(nodes.count); - - for (int i = 0; i < nodes.count; i++) { - v.push_back([nodes[i] CGSizeValue]); - } - - _nodeSizes.insert(_nodeSizes.begin() + idx, v); - }]; -} - -- (void)deleteSectionsAtIndexSet:(NSIndexSet *)indexSet { - [indexSet enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL *stop) - { - _nodeSizes.erase(_nodeSizes.begin() +idx); - }]; -} - #pragma mark - Visible Indices - (BOOL)shouldUpdateForVisibleIndexPaths:(NSArray *)indexPaths viewportSize:(CGSize)viewportSize rangeType:(ASLayoutRangeType)rangeType { - if (!indexPaths.count) { + if (!indexPaths.count || rangeType >= _rangesByType.size()) { return NO; } - std::pair rangeStartPos, rangeEndPos; - - if (rangeType < _rangeStartPos.size() && rangeType < _rangeEndPos.size()) { - rangeStartPos = _rangeStartPos[rangeType]; - rangeEndPos = _rangeEndPos[rangeType]; - } - - std::pair startPos, endPos; - ASFindIndexPathRange(indexPaths, startPos, endPos); - - if (rangeStartPos >= startPos || rangeEndPos <= endPos) { + ASIndexPathRange existingRange = _rangesByType[rangeType]; + ASIndexPathRange newRange = [self indexPathRangeForIndexPaths:indexPaths]; + + ASIndexPath maximumStart = ASIndexPathMaximum(existingRange.start, newRange.start); + ASIndexPath minimumEnd = ASIndexPathMinimum(existingRange.end, newRange.end); + + if (ASIndexPathEqualToIndexPath(maximumStart, existingRange.start) || ASIndexPathEqualToIndexPath(minimumEnd, existingRange.end)) { return YES; } - return ASFlowLayoutDistance(startPos, _visibleRangeStartPos, _nodeSizes) > ASFlowLayoutDistance(_visibleRangeStartPos, rangeStartPos, _nodeSizes) * kASFlowLayoutControllerRefreshingThreshold || - ASFlowLayoutDistance(endPos, _visibleRangeEndPos, _nodeSizes) > ASFlowLayoutDistance(_visibleRangeEndPos, rangeEndPos, _nodeSizes) * kASFlowLayoutControllerRefreshingThreshold; + NSInteger newStartDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.start, newRange.start)]; + NSInteger existingStartDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.start, existingRange.start)] * kASFlowLayoutControllerRefreshingThreshold; + + NSInteger newEndDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.end, newRange.end)]; + NSInteger existingEndDelta = [self flowLayoutDistanceForRange:ASIndexPathRangeMake(_visibleRange.end, existingRange.end)] * kASFlowLayoutControllerRefreshingThreshold; + + return (newStartDelta > existingStartDelta) || (newEndDelta > existingEndDelta); } - (void)setVisibleNodeIndexPaths:(NSArray *)indexPaths { - ASFindIndexPathRange(indexPaths, _visibleRangeStartPos, _visibleRangeEndPos); + _visibleRange = [self indexPathRangeForIndexPaths:indexPaths]; } /** @@ -139,99 +94,134 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3; CGFloat backScreens = scrollDirection == leadingDirection ? tuningParameters.leadingBufferScreenfuls : tuningParameters.trailingBufferScreenfuls; CGFloat frontScreens = scrollDirection == leadingDirection ? tuningParameters.trailingBufferScreenfuls : tuningParameters.leadingBufferScreenfuls; - std::pair startIter = ASFindIndexForRange(_nodeSizes, _visibleRangeStartPos, - backScreens * viewportScreenMetric, _layoutDirection); - std::pair endIter = ASFindIndexForRange(_nodeSizes, _visibleRangeEndPos, frontScreens * viewportScreenMetric, _layoutDirection); + + ASIndexPath startPath = [self findIndexPathAtDistance:(-backScreens * viewportScreenMetric) fromIndexPath:_visibleRange.start]; + ASIndexPath endPath = [self findIndexPathAtDistance:(frontScreens * viewportScreenMetric) fromIndexPath:_visibleRange.end]; + ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never begin at a further position than endPath"); + NSMutableSet *indexPathSet = [[NSMutableSet alloc] init]; - while (startIter != endIter) { - [indexPathSet addObject:[NSIndexPath indexPathForRow:startIter.second inSection:startIter.first]]; - startIter.second++; + NSArray *completedNodes = [_dataSource completedNodes]; + + while (!ASIndexPathEqualToIndexPath(startPath, endPath)) { + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:startPath]]; + startPath.row++; - while (startIter.second == _nodeSizes[startIter.first].size() && startIter.first < _nodeSizes.size()) { - startIter.second = 0; - startIter.first++; + // Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. + while (startPath.row >= [(NSArray *)completedNodes[startPath.section] count] && startPath.section < completedNodes.count - 1) { + startPath.row = 0; + startPath.section++; + ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never reach a further section than endPath"); } } - [indexPathSet addObject:[NSIndexPath indexPathForRow:endIter.second inSection:endIter.first]]; + [indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:endPath]]; return indexPathSet; } #pragma mark - Utility -static void ASFindIndexPathRange(NSArray *indexPaths, std::pair &startPos, std::pair &endPos) - +- (ASIndexPathRange)indexPathRangeForIndexPaths:(NSArray *)indexPaths { - NSIndexPath *initialIndexPath = [indexPaths firstObject]; - startPos = endPos = {initialIndexPath.section, initialIndexPath.row}; + // Set up an initial value so the MIN and MAX can work in the enumeration. + __block ASIndexPath currentIndexPath = [[indexPaths firstObject] ASIndexPathValue]; + __block ASIndexPathRange range; + range.start = currentIndexPath; + range.end = currentIndexPath; + [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) { - std::pair p(indexPath.section, indexPath.row); - startPos = MIN(startPos, p); - endPos = MAX(endPos, p); + currentIndexPath = [indexPath ASIndexPathValue]; + range.start = ASIndexPathMinimum(range.start, currentIndexPath); + range.end = ASIndexPathMaximum(range.end, currentIndexPath); }]; + return range; } -static const std::pair ASFindIndexForRange(const std::vector> &nodes, - const std::pair &pos, - CGFloat range, - ASFlowLayoutDirection layoutDirection) +- (ASIndexPath)findIndexPathAtDistance:(CGFloat)distance fromIndexPath:(ASIndexPath)start { - std::pair cur = pos, pre = pos; + // "end" is the index path we'll advance until we have gone far enough from "start" to reach "distance" + ASIndexPath end = start; + // "previous" will store one iteration before "end", in case we go too far and need to reset "end" to be "previous" + ASIndexPath previous = start; - if (range < 0.0 && cur.first >= 0 && cur.first < nodes.size() && cur.second >= 0 && cur.second < nodes[cur.first].size()) { - // search backward - while (range < 0.0 && cur.first >= 0 && cur.second >= 0) { - pre = cur; - CGSize size = nodes[cur.first][cur.second]; - range += layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; - cur.second--; - while (cur.second < 0 && cur.first > 0) { - cur.second = (int)nodes[--cur.first].size() - 1; + NSArray *completedNodes = [_dataSource completedNodes]; + NSUInteger numberOfSections = [completedNodes count]; + NSUInteger numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + + // If "distance" is negative, advance "end" backwards across rows and sections. + // Otherwise, advance forward. In either case, bring "distance" closer to zero by the dimension of each row passed. + if (distance < 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) { + while (distance < 0.0 && end.section >= 0 && end.row >= 0) { + previous = end; + ASDisplayNode *node = completedNodes[end.section][end.row]; + CGSize size = node.calculatedSize; + distance += (_layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height); + end.row--; + // If we've gone to a negative row, set to the last row of the previous section. While loop is required to handle empty sections. + while (end.row < 0 && end.section > 0) { + end.section--; + numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; + end.row = numberOfRowsInSection - 1; } } - if (cur.second < 0) { - cur = pre; + if (end.row < 0) { + end = previous; } } else { - // search forward - while (range > 0.0 && cur.first >= 0 && cur.first < nodes.size() && cur.second >= 0 && cur.second < nodes[cur.first].size()) { - pre = cur; - CGSize size = nodes[cur.first][cur.second]; - range -= layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; + while (distance > 0.0 && end.section >= 0 && end.section < numberOfSections && end.row >= 0 && end.row < numberOfRowsInSection) { + previous = end; + ASDisplayNode *node = completedNodes[end.section][end.row]; + CGSize size = node.calculatedSize; + distance -= _layoutDirection == ASFlowLayoutDirectionHorizontal ? size.width : size.height; - cur.second++; - while (cur.second == nodes[cur.first].size() && cur.first < (int)nodes.size() - 1) { - cur.second = 0; - cur.first++; + end.row++; + // If we've gone beyond the section, reset to the beginning of the next section. While loop is required to handle empty sections. + while (end.row >= numberOfRowsInSection && end.section < numberOfSections - 1) { + end.row = 0; + end.section++; + numberOfRowsInSection = [(NSArray *)completedNodes[end.section] count]; } } - if (cur.second == nodes[cur.first].size()) { - cur = pre; + if (end.row >= numberOfRowsInSection) { + end = previous; } } - return cur; + return end; } -static int ASFlowLayoutDistance(const std::pair &start, const std::pair &end, const std::vector> &nodes) +- (NSInteger)flowLayoutDistanceForRange:(ASIndexPathRange)range { - if (start == end) { + // This method should only be called with the range in proper order (start comes before end). + ASDisplayNodeAssert(ASIndexPathEqualToIndexPath(ASIndexPathMinimum(range.start, range.end), range.start), @"flowLayoutDistanceForRange: called with invalid range"); + + if (ASIndexPathEqualToIndexPath(range.start, range.end)) { return 0; - } else if (start > end) { - return - ASFlowLayoutDistance(end, start, nodes); } + + NSInteger totalRowCount = 0; + NSUInteger numberOfRowsInSection = 0; + NSArray *completedNodes = [_dataSource completedNodes]; - int res = 0; - - for (int i = start.first; i <= end.first; i++) { - res += (i == end.first ? end.second + 1 : nodes[i].size()) - (i == start.first ? start.second : 0); + for (NSInteger section = range.start.section; section <= range.end.section; section++) { + numberOfRowsInSection = [(NSArray *)completedNodes[section] count]; + totalRowCount += numberOfRowsInSection; + + if (section == range.start.section) { + // For the start section, make sure we don't count the rows before the start row. + totalRowCount -= range.start.row; + } else if (section == range.end.section) { + // For the start section, make sure we don't count the rows after the end row. + totalRowCount -= (numberOfRowsInSection - (range.end.row + 1)); + } } - - return res; + + ASDisplayNodeAssert(totalRowCount >= 0, @"totalRowCount in flowLayoutDistanceForRange: should not be negative"); + return totalRowCount; } @end diff --git a/AsyncDisplayKit/Details/ASIndexPath.h b/AsyncDisplayKit/Details/ASIndexPath.h new file mode 100644 index 0000000000..9eed7dd247 --- /dev/null +++ b/AsyncDisplayKit/Details/ASIndexPath.h @@ -0,0 +1,84 @@ +// +// ASIndexPath.h +// Pods +// +// Created by Scott Goodson on 7/4/15. +// +// A much more efficient way to handle index paths than NSIndexPath. +// For best results, use C++ vectors; NSValue wrapping with Cocoa collections +// would make NSIndexPath a much better choice. +// + +typedef struct { + NSInteger section; + NSInteger row; +} ASIndexPath; + +typedef struct { + ASIndexPath start; + ASIndexPath end; +} ASIndexPathRange; + +ASIndexPath ASIndexPathMake(NSInteger section, NSInteger row) +{ + ASIndexPath indexPath; + indexPath.section = section; + indexPath.row = row; + return indexPath; +} + +BOOL ASIndexPathEqualToIndexPath(ASIndexPath first, ASIndexPath second) +{ + return (first.section == second.section && first.row == second.row); +} + +ASIndexPath ASIndexPathMinimum(ASIndexPath first, ASIndexPath second) +{ + if (first.section < second.section) { + return first; + } else if (first.section > second.section) { + return second; + } else { + return (first.row < second.row ? first : second); + } +} + +ASIndexPath ASIndexPathMaximum(ASIndexPath first, ASIndexPath second) +{ + if (first.section > second.section) { + return first; + } else if (first.section < second.section) { + return second; + } else { + return (first.row > second.row ? first : second); + } +} + +ASIndexPathRange ASIndexPathRangeMake(ASIndexPath first, ASIndexPath second) +{ + ASIndexPathRange range; + range.start = ASIndexPathMinimum(first, second); + range.end = ASIndexPathMaximum(first, second); + return range; +} + +BOOL ASIndexPathRangeEqualToIndexPathRange(ASIndexPathRange first, ASIndexPathRange second) +{ + return ASIndexPathEqualToIndexPath(first.start, second.start) && ASIndexPathEqualToIndexPath(first.end, second.end); +} + +@interface NSIndexPath (ASIndexPathAdditions) ++ (NSIndexPath *)indexPathWithASIndexPath:(ASIndexPath)indexPath; +- (ASIndexPath)ASIndexPathValue; +@end + +@implementation NSIndexPath (ASIndexPathAdditions) ++ (NSIndexPath *)indexPathWithASIndexPath:(ASIndexPath)indexPath +{ + return [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];; +} +- (ASIndexPath)ASIndexPathValue +{ + return ASIndexPathMake(self.section, self.row); +} +@end diff --git a/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h index b7fbbd8c3f..d5058a8622 100644 --- a/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h +++ b/AsyncDisplayKit/Details/ASMultidimensionalArrayUtils.h @@ -44,7 +44,7 @@ extern NSArray *ASFindElementsInMultidimensionalArrayAtIndexPaths(NSMutableArray 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); diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index f59b151f2e..cdfb0568b4 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -107,26 +107,4 @@ */ - (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 diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 2ac2593da2..f41d9d291c 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -111,6 +111,7 @@ NSMutableSet *removedIndexPaths = _rangeIsValid ? [[_rangeTypeIndexPaths objectForKey:rangeKey] mutableCopy] : [NSMutableSet set]; [removedIndexPaths minusSet:indexPaths]; [removedIndexPaths minusSet:visibleNodePathsSet]; + if (removedIndexPaths.count) { NSArray *removedNodes = [_delegate rangeController:self nodesAtIndexPaths:[removedIndexPaths allObjects]]; [removedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) { @@ -129,7 +130,7 @@ if ([self shouldSkipVisibleNodesForRangeType:rangeType]) { [addedIndexPaths minusSet:visibleNodePathsSet]; } - + if (addedIndexPaths.count) { NSArray *addedNodes = [_delegate rangeController:self nodesAtIndexPaths:[addedIndexPaths allObjects]]; [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 { ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path"); @@ -198,40 +191,18 @@ }]; ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(insertNodesAtIndexPaths:withSizes:)]) { - [_layoutController insertNodesAtIndexPaths:indexPaths withSizes:nodeSizes]; - } _rangeIsValid = NO; [_delegate rangeController:self didInsertNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); } -- (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 { ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(deleteNodesAtIndexPaths:)]) { - [_layoutController deleteNodesAtIndexPaths:indexPaths]; - } _rangeIsValid = NO; [_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; }); } -- (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 { ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections"); @@ -246,27 +217,13 @@ }]; ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(insertSections:atIndexSet:)]) { - [_layoutController insertSections:sectionNodeSizes atIndexSet:indexSet]; - } _rangeIsValid = NO; [_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); } -- (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 { ASDisplayNodePerformBlockOnMainThread(^{ - if ([_layoutController respondsToSelector:@selector(deleteSectionsAtIndexSet:)]) { - [_layoutController deleteSectionsAtIndexSet:indexSet]; - } _rangeIsValid = NO; [_delegate rangeController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOptions:animationOptions]; }); diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index 1bebdb9815..8b5300269d 100644 --- a/AsyncDisplayKit/Details/_ASDisplayView.mm +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -243,6 +243,14 @@ [_node tintColorDidChange]; } +- (BOOL)canBecomeFirstResponder { + return [_node canBecomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + return [_node canResignFirstResponder]; +} + - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { // We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:. diff --git a/AsyncDisplayKitTests/ASDisplayNodeTests.m b/AsyncDisplayKitTests/ASDisplayNodeTests.m index fb40acadef..4ce0c49bcf 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeTests.m +++ b/AsyncDisplayKitTests/ASDisplayNodeTests.m @@ -71,6 +71,9 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @property (atomic, copy) CGSize(^calculateSizeBlock)(ASTestDisplayNode *node, CGSize size); @end +@interface ASTestResponderNode : ASTestDisplayNode +@end + @implementation ASTestDisplayNode - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize @@ -91,9 +94,57 @@ for (ASDisplayNode *n in @[ nodes ]) {\ @interface UIDisplayNodeTestView : UIView @end +@interface UIResponderNodeTestView : _ASDisplayView +@property(nonatomic) BOOL isFirstResponder; +@end + @implementation UIDisplayNodeTestView @end +@interface ASTestWindow : UIWindow +@end + +@implementation ASTestWindow + +- (id)firstResponder { + return self.subviews.firstObject; +} + +@end + +@implementation ASTestResponderNode + ++ (Class)viewClass { + return [UIResponderNodeTestView class]; +} + +- (BOOL)canBecomeFirstResponder { + return YES; +} + +@end + +@implementation UIResponderNodeTestView + +- (BOOL)becomeFirstResponder { + self.isFirstResponder = YES; + return YES; +} + +- (BOOL)canResignFirstResponder { + return YES; +} + +- (BOOL)resignFirstResponder { + if (self.isFirstResponder) { + self.isFirstResponder = NO; + return YES; + } + return NO; +} + +@end + @interface ASDisplayNodeTests : XCTestCase @end @@ -102,6 +153,25 @@ for (ASDisplayNode *n in @[ nodes ]) {\ dispatch_queue_t queue; } +- (void)testOverriddenFirstResponderBehavior { + ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertTrue([node becomeFirstResponder]); +} + +- (void)testDefaultFirstResponderBehavior { + ASTestDisplayNode *node = [[ASTestDisplayNode alloc] init]; + XCTAssertFalse([node canBecomeFirstResponder]); + XCTAssertFalse([node becomeFirstResponder]); +} + +- (void)testLayerBackedFirstResponderBehavior { + ASTestDisplayNode *node = [[ASTestResponderNode alloc] init]; + node.layerBacked = YES; + XCTAssertTrue([node canBecomeFirstResponder]); + XCTAssertFalse([node becomeFirstResponder]); +} + - (void)setUp { [super setUp]; diff --git a/AsyncDisplayKitTests/ASTableViewTests.m b/AsyncDisplayKitTests/ASTableViewTests.m index 84f7f29eee..08531ce131 100644 --- a/AsyncDisplayKitTests/ASTableViewTests.m +++ b/AsyncDisplayKitTests/ASTableViewTests.m @@ -113,6 +113,33 @@ 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 { // Keep the viewport moderately sized so that new cells are loaded on scrolling @@ -127,22 +154,34 @@ [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) { - NSInteger randA = arc4random_uniform(NumberOfSections - 1); - NSInteger randB = arc4random_uniform(NumberOfSections - 1); + UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : UITableViewRowAnimationNone); - [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]; + } } } diff --git a/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme b/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme index 1e14aa0329..5c91bfc64d 100644 --- a/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme +++ b/examples/ASTableViewStressTest/Sample.xcodeproj/xcshareddata/xcschemes/Sample.xcscheme @@ -1,6 +1,6 @@ { ASTableView *_tableView; + NSMutableArray *_sections; // Contains arrays of indexPaths representing rows } @end @@ -28,18 +29,24 @@ @implementation ViewController -#pragma mark - -#pragma mark UIViewController. - - (instancetype)init { if (!(self = [super init])) return nil; _tableView = [[ASTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain asyncDataFetching:YES]; - _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // KittenNode has its own separator _tableView.asyncDataSource = 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; } @@ -63,42 +70,99 @@ [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 { - // Keep the viewport moderately sized so that new cells are loaded on scrolling - ASTableView *tableView = [[ASTableView alloc] initWithFrame:CGRectMake(0, 0, 100, 500) - style:UITableViewStylePlain - asyncDataFetching:NO]; + _tableView.asyncDelegate = self; + _tableView.asyncDataSource = self; - tableView.asyncDelegate = self; - tableView.asyncDataSource = self; - - [tableView reloadData]; - - [tableView reloadSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(1,2)] withRowAnimation:UITableViewRowAnimationNone]; + [_tableView reloadData]; + NSArray *indexPathsAddedAndRemoved = nil; + for (int i = 0; i < NumberOfReloadIterations; ++i) { - NSInteger randA = arc4random_uniform(NumberOfSections - 1); - NSInteger randB = arc4random_uniform(NumberOfSections - 1); + UITableViewRowAnimation rowAnimation = (arc4random_uniform(1) == 0 ? UITableViewRowAnimationMiddle : 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 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 { - return NumberOfSections; + return _sections.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { - return NumberOfRowsPerSection; + return [(NSArray *)[_sections objectAtIndex:section] count]; } - (ASCellNode *)tableView:(ASTableView *)tableView nodeForRowAtIndexPath:(NSIndexPath *)indexPath