mirror of
https://github.com/HackPlan/AsyncDisplayKit.git
synced 2026-04-01 08:45:09 +08:00
Merge branch 'master' into layout_node
This commit is contained in:
@@ -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/'
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
// use UITableViewCell defaults
|
||||
_selectionStyle = UITableViewCellSelectionStyleDefault;
|
||||
self.clipsToBounds = YES;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
84
AsyncDisplayKit/Details/ASIndexPath.h
Normal file
84
AsyncDisplayKit/Details/ASIndexPath.h
Normal 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
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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:.
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0620"
|
||||
LastUpgradeVersion = "0630"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user