Add support automatically adjusting the content offset to UITableView as well as support for performing endUpdates with no animations. Additionally, there are critical bug fixes for ASDataController (begin/end updates delegates not called in correct order) and ASRangeController (failure to fully refresh internal state when inserts or delete are made.)

This commit is contained in:
Ethan Nagel
2015-07-20 14:05:56 -07:00
parent d5086bd64f
commit f497639124
9 changed files with 255 additions and 40 deletions

View File

@@ -78,6 +78,30 @@
*/ */
@property (nonatomic, assign) CGFloat leadingScreensForBatching; @property (nonatomic, assign) CGFloat leadingScreensForBatching;
/**
* Perform a batch of updates asynchronously, optionally disabling all animations in the batch. You can call it from background
* thread (it is recommendated) and the UI collection view will be updated asynchronously. The asyncDataSource must be updated
* to reflect the changes before this method is called.
*
* @param animated NO to disable animations for this batch
* @param updates The block that performs the relevant insert, delete, reload, or move operations.
* @param completion A completion handler block to execute when all of the operations are finished. This block takes a single
* Boolean parameter that contains the value YES if all of the related animations completed successfully or
* NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.
*/
- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion;
/**
* Perform a batch of updates asynchronously. You can call it from background thread (it is recommendated) and the UI collection
* view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes before this method is called.
*
* @param updates The block that performs the relevant insert, delete, reload, or move operations.
* @param completion A completion handler block to execute when all of the operations are finished. This block takes a single
* Boolean parameter that contains the value YES if all of the related animations completed successfully or
* NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.
*/
- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion;
/** /**
* Reload everything from scratch, destroying the working range and all cached nodes. * Reload everything from scratch, destroying the working range and all cached nodes.
* *

View File

@@ -281,11 +281,16 @@ static BOOL _isInterceptedSelector(SEL sel)
#pragma mark Assertions. #pragma mark Assertions.
- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion - (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion
{ {
[_dataController beginUpdates]; [_dataController beginUpdates];
updates(); updates();
[_dataController endUpdatesWithCompletion:completion]; [_dataController endUpdatesAnimated:animated completion:completion];
}
- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion
{
[self performBatchAnimated:YES updates:updates completion:completion];
} }
- (void)insertSections:(NSIndexSet *)sections - (void)insertSections:(NSIndexSet *)sections
@@ -540,7 +545,7 @@ static BOOL _isInterceptedSelector(SEL sel)
_performingBatchUpdates = YES; _performingBatchUpdates = YES;
} }
- (void)rangeControllerEndUpdates:(ASRangeController *)rangeController completion:(void (^)(BOOL))completion { - (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
@@ -550,11 +555,21 @@ static BOOL _isInterceptedSelector(SEL sel)
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
BOOL animationsEnabled = NO;
if (!animated) {
animationsEnabled = [UIView areAnimationsEnabled];
[UIView setAnimationsEnabled:NO];
}
[super performBatchUpdates:^{ [super performBatchUpdates:^{
[_batchUpdateBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { [_batchUpdateBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) {
block(); block();
}]; }];
} completion:^(BOOL finished) { } completion:^(BOOL finished) {
if (!animated) {
[UIView setAnimationsEnabled:animationsEnabled];
}
if (completion) { if (completion) {
completion(finished); completion(finished);
} }
@@ -581,7 +596,7 @@ static BOOL _isInterceptedSelector(SEL sel)
return [_dataController nodesAtIndexPaths:indexPaths]; return [_dataController nodesAtIndexPaths:indexPaths];
} }
- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
@@ -600,7 +615,7 @@ static BOOL _isInterceptedSelector(SEL sel)
} }
} }
- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();

View File

@@ -93,15 +93,40 @@
*/ */
- (void)reloadData; - (void)reloadData;
/** /**
* We don't support the these methods for animation yet. * begins a batch of insert, delete reload and move operations. Batches are asynchronous an thread safe.
*
* TODO: support animations.
*/ */
- (void)beginUpdates; - (void)beginUpdates;
/**
* Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view.
* You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations
* to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating
* the operations simultaneously. This method is asynchronous and thread safe. It's important to remeber that the ASTableView will
* be processing the updates asynchronously after this call is completed.
*
* @param animated NO to disable all animations.
* @param completion A completion handler block to execute when all of the operations are finished. This block takes a single
* Boolean parameter that contains the value YES if all of the related animations completed successfully or
* NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.
*/
- (void)endUpdates; - (void)endUpdates;
/**
* Concludes a series of method calls that insert, delete, select, or reload rows and sections of the table view.
* You call this method to bracket a series of method calls that begins with beginUpdates and that consists of operations
* to insert, delete, select, and reload rows and sections of the table view. When you call endUpdates, ASTableView begins animating
* the operations simultaneously. This method is asynchronous and thread safe. It's important to remeber that the ASTableView will
* be processing the updates asynchronously after this call and are not guaranteed to be reflected in the ASTableView until
* the completion block is executed.
*
* @param animated NO to disable all animations.
* @param completion A completion handler block to execute when all of the operations are finished. This block takes a single
* Boolean parameter that contains the value YES if all of the related animations completed successfully or
* NO if they were interrupted. This parameter may be nil. If supplied, the block is run on the main thread.
*/
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion;
/** /**
* Inserts one or more sections, with an option to animate the insertion. * Inserts one or more sections, with an option to animate the insertion.
* *
@@ -222,6 +247,14 @@
*/ */
- (NSArray *)visibleNodes; - (NSArray *)visibleNodes;
/**
* YES to automatically adjust the contentOffset when cells are inserted or deleted "before"
* visible cells, maintaining the users' visible scroll position.
*
* default is NO.
*/
@property (nonatomic) BOOL automaticallyAdjustsContentOffset;
@end @end

View File

@@ -16,6 +16,8 @@
#import "ASDisplayNodeInternal.h" #import "ASDisplayNodeInternal.h"
#import "ASBatchFetching.h" #import "ASBatchFetching.h"
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
#pragma mark - #pragma mark -
#pragma mark Proxying. #pragma mark Proxying.
@@ -126,6 +128,9 @@ static BOOL _isInterceptedSelector(SEL sel)
ASBatchContext *_batchContext; ASBatchContext *_batchContext;
NSIndexPath *_pendingVisibleIndexPath; NSIndexPath *_pendingVisibleIndexPath;
NSIndexPath *_contentOffsetAdjustmentTopVisibleRow;
CGFloat _contentOffsetAdjustment;
} }
@property (atomic, assign) BOOL asyncDataSourceLocked; @property (atomic, assign) BOOL asyncDataSourceLocked;
@@ -176,6 +181,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
_leadingScreensForBatching = 1.0; _leadingScreensForBatching = 1.0;
_batchContext = [[ASBatchContext alloc] init]; _batchContext = [[ASBatchContext alloc] init];
_automaticallyAdjustsContentOffset = NO;
} }
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
@@ -326,9 +333,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)endUpdates - (void)endUpdates
{ {
[_dataController endUpdates]; [self endUpdatesAnimated:YES completion:nil];
} }
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion;
{
[_dataController endUpdatesAnimated:animated completion:completion];
}
#pragma mark - #pragma mark -
#pragma mark Editing #pragma mark Editing
@@ -373,6 +384,57 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone]; [_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:UITableViewRowAnimationNone];
} }
#pragma mark -
#pragma mark adjust content offset
- (void)beginAdjustingContentOffset
{
ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES");
_contentOffsetAdjustment = 0;
_contentOffsetAdjustmentTopVisibleRow = self.indexPathsForVisibleRows.firstObject;
}
- (void)endAdjustingContentOffset
{
ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES");
if (_contentOffsetAdjustment != 0) {
self.contentOffset = CGPointMake(0, self.contentOffset.y+_contentOffsetAdjustment);
}
_contentOffsetAdjustment = 0;
_contentOffsetAdjustmentTopVisibleRow = nil;
}
- (void)adjustContentOffsetWithNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths inserting:(BOOL)inserting {
// Maintain the users visible window when inserting or deleteing cells by adjusting the content offset for nodes
// before the visible area. If in a begin/end updates block this will update _contentOffsetAdjustment, otherwise it will
// update self.contentOffset directly.
ASDisplayNodeAssert(_automaticallyAdjustsContentOffset, @"this method should only be called when _automaticallyAdjustsContentOffset == YES");
CGFloat dir = (inserting) ? +1 : -1;
CGFloat adjustment = 0;
NSIndexPath *top = _contentOffsetAdjustmentTopVisibleRow ?: self.indexPathsForVisibleRows.firstObject;
for (int index=0; index<indexPaths.count; index++) {
NSIndexPath *indexPath = indexPaths[index];
if ([indexPath compare:top] <= 0) { // if this row is before or equal to the topmost visible row, make adjustments...
ASCellNode *cellNode = nodes[index];
adjustment += cellNode.calculatedSize.height * dir;
if (indexPath.section == top.section) {
top = [NSIndexPath indexPathForRow:top.row+dir inSection:top.section];
}
}
}
if (_contentOffsetAdjustmentTopVisibleRow) { // true of we are in a begin/end update block (see beginAdjustingContentOffset)
_contentOffsetAdjustmentTopVisibleRow = top;
_contentOffsetAdjustment += adjustment;
} else if (adjustment != 0) {
self.contentOffset = CGPointMake(0, self.contentOffset.y+adjustment);
}
}
#pragma mark - #pragma mark -
#pragma mark Intercepted selectors #pragma mark Intercepted selectors
@@ -499,17 +561,23 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController - (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"--- UITableView beginUpdates");
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
[super beginUpdates]; [super beginUpdates];
if (_automaticallyAdjustsContentOffset) {
[self beginAdjustingContentOffset];
}
} }
- (void)rangeControllerEndUpdates:(ASRangeController *)rangeController completion:(void (^)(BOOL))completion - (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"--- UITableView endUpdates");
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
if (completion) { if (completion) {
@@ -518,7 +586,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
if (_automaticallyAdjustsContentOffset) {
[self endAdjustingContentOffset];
}
ASPerformBlockWithoutAnimation(!animated, ^{
[super endUpdates]; [super endUpdates];
});
if (completion) { if (completion) {
completion(YES); completion(YES);
@@ -586,9 +660,10 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
return self.bounds.size; return self.bounds.size;
} }
- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"UITableView insertRows:%ld rows", indexPaths.count);
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -598,11 +673,16 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
ASPerformBlockWithoutAnimation(preventAnimation, ^{ ASPerformBlockWithoutAnimation(preventAnimation, ^{
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions];
}); });
if (_automaticallyAdjustsContentOffset) {
[self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:YES];
}
} }
- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"UITableView deleteRows:%ld rows", indexPaths.count);
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -612,11 +692,17 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
ASPerformBlockWithoutAnimation(preventAnimation, ^{ ASPerformBlockWithoutAnimation(preventAnimation, ^{
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions];
}); });
if (_automaticallyAdjustsContentOffset) {
[self adjustContentOffsetWithNodes:nodes atIndexPaths:indexPaths inserting:NO];
}
} }
- (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)rangeController:(ASRangeController *)rangeController didInsertSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"UITableView insertSections:%@", indexSet);
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -631,6 +717,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions - (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"UITableView deleteSections:%@", indexSet);
if (!self.asyncDataSource) { if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes

View File

@@ -65,7 +65,7 @@ typedef NSUInteger ASDataControllerAnimationOptions;
Called for batch update. Called for batch update.
*/ */
- (void)dataControllerBeginUpdates:(ASDataController *)dataController; - (void)dataControllerBeginUpdates:(ASDataController *)dataController;
- (void)dataControllerEndUpdates:(ASDataController *)dataController completion:(void (^)(BOOL))completion; - (void)dataController:(ASDataController *)dataController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion;
/** /**
Called for insertion of elements. Called for insertion of elements.
@@ -75,7 +75,7 @@ typedef NSUInteger ASDataControllerAnimationOptions;
/** /**
Called for deletion of elements. Called for deletion of elements.
*/ */
- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/** /**
Called for insertion of sections. Called for insertion of sections.
@@ -138,7 +138,7 @@ typedef NSUInteger ASDataControllerAnimationOptions;
- (void)endUpdates; - (void)endUpdates;
- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion; - (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion;
- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;

View File

@@ -16,6 +16,9 @@
#import "ASMultidimensionalArrayUtils.h" #import "ASMultidimensionalArrayUtils.h"
#import "ASDisplayNodeInternal.h" #import "ASDisplayNodeInternal.h"
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
const static NSUInteger kASDataControllerSizingCountPerProcessor = 5; const static NSUInteger kASDataControllerSizingCountPerProcessor = 5;
static void *kASSizingQueueContext = &kASSizingQueueContext; static void *kASSizingQueueContext = &kASSizingQueueContext;
@@ -73,7 +76,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
// Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later. // Interrogate our delegate to understand its capabilities, optimizing away expensive respondsToSelector: calls later.
_delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)]; _delegateDidInsertNodes = [_delegate respondsToSelector:@selector(dataController:didInsertNodes:atIndexPaths:withAnimationOptions:)];
_delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodesAtIndexPaths:withAnimationOptions:)]; _delegateDidDeleteNodes = [_delegate respondsToSelector:@selector(dataController:didDeleteNodes:atIndexPaths:withAnimationOptions:)];
_delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)]; _delegateDidInsertSections = [_delegate respondsToSelector:@selector(dataController:didInsertSections:atIndexSet:withAnimationOptions:)];
_delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)]; _delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)];
} }
@@ -164,12 +167,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
if (indexPaths.count == 0) if (indexPaths.count == 0)
return; return;
LOG(@"_deleteNodesAtIndexPaths:%@, full index paths in _editingNodes = %@", indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes));
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths);
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths);
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths); ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths);
if (_delegateDidDeleteNodes) if (_delegateDidDeleteNodes)
[_delegate dataController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [_delegate dataController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}); });
} }
@@ -242,6 +247,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; [self _populateFromEntireDataSourceWithMutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - reloadData");
// Remove everything that existed before the reload, now that we're ready to insert replacements // Remove everything that existed before the reload, now that we're ready to insert replacements
NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes); NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -318,6 +325,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)beginUpdates - (void)beginUpdates
{ {
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
// Begin queuing up edit calls that happen on the main thread. // Begin queuing up edit calls that happen on the main thread.
// This will prevent further operations from being scheduled on _editingTransactionQueue. // This will prevent further operations from being scheduled on _editingTransactionQueue.
// It's fine if there is an in-flight operation on _editingTransactionQueue, // It's fine if there is an in-flight operation on _editingTransactionQueue,
@@ -327,23 +335,38 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)endUpdates - (void)endUpdates
{ {
[self endUpdatesWithCompletion:NULL]; [self endUpdatesAnimated:YES completion:nil];
} }
- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion - (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{ {
_batchUpdateCounter--; _batchUpdateCounter--;
if (_batchUpdateCounter == 0) { if (_batchUpdateCounter == 0) {
LOG(@"endUpdatesWithCompletion - beginning");
[_editingTransactionQueue addOperationWithBlock:^{
ASDisplayNodePerformBlockOnMainThread(^{
LOG(@"endUpdatesWithCompletion - begin updates call to delegate");
[_delegate dataControllerBeginUpdates:self]; [_delegate dataControllerBeginUpdates:self];
});
}];
// Running these commands may result in blocking on an _editingTransactionQueue operation that started even before -beginUpdates. // 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. // Each subsequent command in the queue will also wait on the full asynchronous completion of the prior command's edit transaction.
LOG(@"endUpdatesWithCompletion - %zd blocks to run", _pendingEditCommandBlocks.count);
[_pendingEditCommandBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) { [_pendingEditCommandBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) {
LOG(@"endUpdatesWithCompletion - running block #%zd", idx);
block(); block();
}]; }];
[_pendingEditCommandBlocks removeAllObjects]; [_pendingEditCommandBlocks removeAllObjects];
[_delegate dataControllerEndUpdates:self completion:completion]; [_editingTransactionQueue addOperationWithBlock:^{
ASDisplayNodePerformBlockOnMainThread(^{
LOG(@"endUpdatesWithCompletion - calling delegate end");
[_delegate dataController:self endUpdatesAnimated:animated completion:completion];
});
}];
} }
} }
@@ -364,6 +387,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - insertSections: %@", indexSet);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{ [self accessDataSourceWithBlock:^{
@@ -372,6 +396,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths]; [self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - insertSections: %@", indexSet);
NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count]; NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count];
for (NSUInteger i = 0; i < indexSet.count; i++) { for (NSUInteger i = 0; i < indexSet.count; i++) {
[sectionArray addObject:[NSMutableArray array]]; [sectionArray addObject:[NSMutableArray array]];
@@ -388,10 +413,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - deleteSections: %@", indexSet);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
// remove elements // remove elements
LOG(@"Edit Transaction - deleteSections: %@", indexSet);
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet); NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -404,6 +431,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - reloadSections: %@", sections);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{ [self accessDataSourceWithBlock:^{
@@ -417,6 +446,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections); NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections);
LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes));
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
// reinsert the elements // reinsert the elements
@@ -430,10 +462,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - moveSection");
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
// remove elements // remove elements
LOG(@"Edit Transaction - moveSection");
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]); NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]);
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -457,6 +494,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - insertRows: %@", indexPaths);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{ [self accessDataSourceWithBlock:^{
@@ -468,6 +507,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
} }
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - insertRows: %@", indexPaths);
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}]; }];
}]; }];
@@ -478,12 +518,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - deleteRows: %@", indexPaths);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
// sort indexPath in order to avoid messing up the index when deleting // sort indexPath in order to avoid messing up the index when deleting
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)]; NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - deleteRows: %@", indexPaths);
[self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions];
}]; }];
}]; }];
@@ -493,6 +536,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - reloadRows: %@", indexPaths);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
// Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source. // Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source.
@@ -504,6 +549,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
}]; }];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - reloadRows: %@", indexPaths);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}]; }];
@@ -515,9 +561,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{ {
[self performEditCommandWithBlock:^{ [self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - moveRow: %@ > %@", indexPath, newIndexPath);
[_editingTransactionQueue waitUntilAllOperationsAreFinished]; [_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{ [_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath);
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]); NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]);
NSArray *indexPaths = [NSArray arrayWithObject:indexPath]; NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];

View File

@@ -104,15 +104,17 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;
NSArray *completedNodes = [_dataSource completedNodes]; NSArray *completedNodes = [_dataSource completedNodes];
while (!ASIndexPathEqualToIndexPath(startPath, endPath)) { ASIndexPath currPath = startPath;
[indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:startPath]];
startPath.row++; while (!ASIndexPathEqualToIndexPath(currPath, endPath)) {
[indexPathSet addObject:[NSIndexPath indexPathWithASIndexPath:currPath]];
currPath.row++;
// Once we reach the end of the section, advance to the next one. Keep advancing if the next section is zero-sized. // 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) { while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < completedNodes.count - 1) {
startPath.row = 0; currPath.row = 0;
startPath.section++; currPath.section++;
ASDisplayNodeAssert(startPath.section <= endPath.section, @"startPath should never reach a further section than endPath"); ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath");
} }
} }

View File

@@ -88,7 +88,7 @@
* *
* @param completion Completion block. * @param completion Completion block.
*/ */
- (void)rangeControllerEndUpdates:(ASRangeController * )rangeController completion:(void (^)(BOOL))completion ; - (void)rangeController:(ASRangeController * )rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion;
/** /**
* Fetch nodes at specific index paths. * Fetch nodes at specific index paths.
@@ -108,7 +108,7 @@
* *
* @param animationOptions Animation options. See ASDataControllerAnimationOptions. * @param animationOptions Animation options. See ASDataControllerAnimationOptions.
*/ */
- (void)rangeController:(ASRangeController *)rangeController didInsertNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/** /**
* Called for nodes deletion. * Called for nodes deletion.
@@ -119,7 +119,7 @@
* *
* @param animationOptions Animation options. See ASDataControllerAnimationOptions. * @param animationOptions Animation options. See ASDataControllerAnimationOptions.
*/ */
- (void)rangeController:(ASRangeController *)rangeController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions; - (void)rangeController:(ASRangeController *)rangeController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;
/** /**
* Called for section insertion. * Called for section insertion.

View File

@@ -89,6 +89,12 @@
} }
NSArray *visibleNodePaths = [_delegate rangeControllerVisibleNodeIndexPaths:self]; NSArray *visibleNodePaths = [_delegate rangeControllerVisibleNodeIndexPaths:self];
if ( visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)...
_queuedRangeUpdate = NO;
return ; // don't do anything for this update, but leave _rangeIsValid to make sure we update it later
}
NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
CGSize viewportSize = [_delegate rangeControllerViewportSize:self]; CGSize viewportSize = [_delegate rangeControllerViewportSize:self];
@@ -104,7 +110,7 @@
// this delegate decide what happens when a node is added or removed from a range // this delegate decide what happens when a node is added or removed from a range
id<ASRangeHandler> rangeDelegate = _rangeTypeHandlers[rangeKey]; id<ASRangeHandler> rangeDelegate = _rangeTypeHandlers[rangeKey];
if ([_layoutController shouldUpdateForVisibleIndexPaths:visibleNodePaths viewportSize:viewportSize rangeType:rangeType]) { if (!_rangeIsValid || [_layoutController shouldUpdateForVisibleIndexPaths:visibleNodePaths viewportSize:viewportSize rangeType:rangeType]) {
NSSet *indexPaths = [_layoutController indexPathsForScrolling:_scrollDirection viewportSize:viewportSize rangeType:rangeType]; NSSet *indexPaths = [_layoutController indexPathsForScrolling:_scrollDirection viewportSize:viewportSize rangeType:rangeType];
// Notify to remove indexpaths that are leftover that are not visible or included in the _layoutController calculated paths // Notify to remove indexpaths that are leftover that are not visible or included in the _layoutController calculated paths
@@ -176,9 +182,9 @@
}); });
} }
- (void)dataControllerEndUpdates:(ASDataController *)dataController completion:(void (^)(BOOL))completion { - (void)dataController:(ASDataController *)dataController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
[_delegate rangeControllerEndUpdates:self completion:completion]; [_delegate rangeController:self endUpdatesAnimated:animated completion:completion];
}); });
} }
@@ -192,14 +198,14 @@
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
_rangeIsValid = NO; _rangeIsValid = NO;
[_delegate rangeController:self didInsertNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [_delegate rangeController:self didInsertNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}); });
} }
- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions { - (void)dataController:(ASDataController *)dataController didDeleteNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions {
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
_rangeIsValid = NO; _rangeIsValid = NO;
[_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions]; [_delegate rangeController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}); });
} }