Merge branch 'master' into layout_node

This commit is contained in:
Huy Nguyen
2015-07-06 22:21:32 +07:00
21 changed files with 821 additions and 563 deletions

View File

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

View File

@@ -26,6 +26,7 @@
// use UITableViewCell defaults
_selectionStyle = UITableViewCellSelectionStyleDefault;
self.clipsToBounds = YES;
return self;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<CGRect> _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<CGRect>(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);
}

View File

@@ -8,7 +8,7 @@
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDealloc2MainObject.h>
#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 <ASFlowLayoutControllerDataSource>
/**
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

View File

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

View File

@@ -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 <ASFlowLayoutControllerDataSource> dataSource;
- (instancetype)initWithScrollOption:(ASFlowLayoutDirection)layoutDirection;

View File

@@ -7,111 +7,66 @@
*/
#import "ASFlowLayoutController.h"
#import "ASAssert.h"
#import "ASDisplayNode.h"
#import "ASIndexPath.h"
#include <map>
#include <vector>
#include <cassert>
#import "ASAssert.h"
static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;
@interface ASFlowLayoutController() {
std::vector<std::vector<CGSize> > _nodeSizes;
std::pair<int, int> _visibleRangeStartPos;
std::pair<int, int> _visibleRangeEndPos;
std::vector<std::pair<int, int>> _rangeStartPos;
std::vector<std::pair<int, int>> _rangeEndPos;
@interface ASFlowLayoutController()
{
ASIndexPathRange _visibleRange;
std::vector<ASIndexPathRange> _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<ASIndexPathRange>(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<CGSize> &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<CGSize> &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<CGSize> 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<int, int> rangeStartPos, rangeEndPos;
if (rangeType < _rangeStartPos.size() && rangeType < _rangeEndPos.size()) {
rangeStartPos = _rangeStartPos[rangeType];
rangeEndPos = _rangeEndPos[rangeType];
}
std::pair<int, int> 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<int, int> startIter = ASFindIndexForRange(_nodeSizes, _visibleRangeStartPos, - backScreens * viewportScreenMetric, _layoutDirection);
std::pair<int, int> 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<int, int> &startPos, std::pair<int, int> &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<int, int> 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<int, int> ASFindIndexForRange(const std::vector<std::vector<CGSize>> &nodes,
const std::pair<int, int> &pos,
CGFloat range,
ASFlowLayoutDirection layoutDirection)
- (ASIndexPath)findIndexPathAtDistance:(CGFloat)distance fromIndexPath:(ASIndexPath)start
{
std::pair<int, int> 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<int, int> &start, const std::pair<int, int> &end, const std::vector<std::vector<CGSize>> &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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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];
}
}
}

View File

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

View File

@@ -16,11 +16,12 @@
#define NumberOfSections 10
#define NumberOfRowsPerSection 20
#define NumberOfReloadIterations 50
#define NumberOfReloadIterations 500
@interface ViewController () <ASTableViewDataSource, ASTableViewDelegate>
{
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