Merge pull request #575 from eanagel/new-auto-adjust-content

Take 2: Automatically adjust content offset for insertion/deletion
This commit is contained in:
appleguy
2015-07-20 22:21:41 -07:00
9 changed files with 291 additions and 74 deletions

View File

@@ -78,6 +78,29 @@
*/
@property (nonatomic, assign) CGFloat leadingScreensForBatching;
/**
* Perform a batch of updates asynchronously, optionally disabling all animations in the batch. This method must be called from the main thread.
* The asyncDataSource must be updated to reflect the changes before the update block completes.
*
* @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. This method must be called from the main thread.
* The asyncDataSource must be updated to reflect the changes before update block completes.
*
* @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.
*
@@ -99,8 +122,7 @@
*
* @param sections An index set that specifies the sections to insert.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)insertSections:(NSIndexSet *)sections;
@@ -110,8 +132,7 @@
*
* @param sections An index set that specifies the sections to delete.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)deleteSections:(NSIndexSet *)sections;
@@ -121,8 +142,7 @@
*
* @param sections An index set that specifies the sections to reload.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)reloadSections:(NSIndexSet *)sections;
@@ -134,8 +154,7 @@
*
* @param newSection The index that is the destination of the move for the section.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;
@@ -145,8 +164,7 @@
*
* @param indexPaths An array of NSIndexPath objects, each representing an item index and section index that together identify an item.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths;
@@ -156,8 +174,7 @@
*
* @param indexPaths An array of NSIndexPath objects identifying the items to delete.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths;
@@ -167,8 +184,7 @@
*
* @param indexPaths An array of NSIndexPath objects identifying the items to reload.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths;
@@ -180,8 +196,7 @@
*
* @param newIndexPath The index path that is the destination of the move for the item.
*
* @discussion This operation is asynchronous and thread safe. 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
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;

View File

@@ -281,50 +281,65 @@ static BOOL _isInterceptedSelector(SEL sel)
#pragma mark Assertions.
- (void)performBatchUpdates:(void (^)())updates completion:(void (^)(BOOL))completion
- (void)performBatchAnimated:(BOOL)animated updates:(void (^)())updates completion:(void (^)(BOOL))completion
{
ASDisplayNodeAssertMainThread();
[_dataController beginUpdates];
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
{
ASDisplayNodeAssertMainThread();
[_dataController insertSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)deleteSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
[_dataController deleteSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)reloadSections:(NSIndexSet *)sections
{
ASDisplayNodeAssertMainThread();
[_dataController reloadSections:sections withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
ASDisplayNodeAssertMainThread();
[_dataController moveSection:section toSection:newSection withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
[_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
[_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths
{
ASDisplayNodeAssertMainThread();
[_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:kASCollectionViewAnimationNone];
}
- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
{
ASDisplayNodeAssertMainThread();
[_dataController moveRowAtIndexPath:indexPath toIndexPath:newIndexPath withAnimationOptions:kASCollectionViewAnimationNone];
}
@@ -540,7 +555,7 @@ static BOOL _isInterceptedSelector(SEL sel)
_performingBatchUpdates = YES;
}
- (void)rangeControllerEndUpdates:(ASRangeController *)rangeController completion:(void (^)(BOOL))completion {
- (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) {
@@ -550,11 +565,21 @@ static BOOL _isInterceptedSelector(SEL sel)
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:^{
[_batchUpdateBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) {
block();
}];
} completion:^(BOOL finished) {
if (!animated) {
[UIView setAnimationsEnabled:animationsEnabled];
}
if (completion) {
completion(finished);
}
@@ -581,7 +606,7 @@ static BOOL _isInterceptedSelector(SEL sel)
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();
@@ -600,7 +625,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();

View File

@@ -93,15 +93,40 @@
*/
- (void)reloadData;
/**
* We don't support the these methods for animation yet.
*
* TODO: support animations.
* begins a batch of insert, delete reload and move operations. This method must be called from the main thread.
*/
- (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 must be called from the main thread. 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;
/**
* 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 must be called from the main thread. 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.
*
@@ -109,8 +134,7 @@
*
* @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
@@ -122,8 +146,7 @@
*
* @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
@@ -135,8 +158,7 @@
*
* @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
@@ -148,8 +170,7 @@
*
* @param newSection The index that is the destination of the move for the section.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection;
@@ -161,8 +182,7 @@
*
* @param animation A constant that indicates how the insertion is to be animated. See UITableViewRowAnimation.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
@@ -174,8 +194,7 @@
*
* @param animation A constant that indicates how the deletion is to be animated. See UITableViewRowAnimation.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
@@ -187,8 +206,7 @@
*
* @param animation A constant that indicates how the reloading is to be animated. See UITableViewRowAnimation.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
@@ -200,8 +218,7 @@
*
* @param newIndexPath The index path that is the destination of the move for the row.
*
* @discussion This operation is asynchronous and thread safe. You can call it from background thread (it is recommendated)
* and the UI table view will be updated asynchronously. The asyncDataSource must be updated to reflect the changes
* @discussion This method must be called from the main thread. The asyncDataSource must be updated to reflect the changes
* before this method is called.
*/
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath;
@@ -222,6 +239,15 @@
*/
- (NSArray *)visibleNodes;
/**
* YES to automatically adjust the contentOffset when cells are inserted or deleted "before"
* visible cells, maintaining the users' visible scroll position. Currently this feature tracks insertions, moves and deletions of
* cells, but section edits are ignored.
*
* default is NO.
*/
@property (nonatomic) BOOL automaticallyAdjustsContentOffset;
@end

View File

@@ -16,6 +16,8 @@
#import "ASDisplayNodeInternal.h"
#import "ASBatchFetching.h"
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
#pragma mark -
#pragma mark Proxying.
@@ -126,6 +128,9 @@ static BOOL _isInterceptedSelector(SEL sel)
ASBatchContext *_batchContext;
NSIndexPath *_pendingVisibleIndexPath;
NSIndexPath *_contentOffsetAdjustmentTopVisibleRow;
CGFloat _contentOffsetAdjustment;
}
@property (atomic, assign) BOOL asyncDataSourceLocked;
@@ -176,6 +181,8 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
_leadingScreensForBatching = 1.0;
_batchContext = [[ASBatchContext alloc] init];
_automaticallyAdjustsContentOffset = NO;
}
- (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style
@@ -321,58 +328,123 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)beginUpdates
{
ASDisplayNodeAssertMainThread();
[_dataController beginUpdates];
}
- (void)endUpdates
{
[_dataController endUpdates];
[self endUpdatesAnimated:YES completion:nil];
}
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL completed))completion;
{
ASDisplayNodeAssertMainThread();
[_dataController endUpdatesAnimated:animated completion:completion];
}
#pragma mark -
#pragma mark Editing
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
[_dataController insertSections:sections withAnimationOptions:animation];
}
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
[_dataController deleteSections:sections withAnimationOptions:animation];
}
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
[_dataController reloadSections:sections withAnimationOptions:animation];
}
- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
ASDisplayNodeAssertMainThread();
[_dataController moveSection:section toSection:newSection withAnimationOptions:UITableViewRowAnimationNone];
}
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
[_dataController insertRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
}
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
[_dataController deleteRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
}
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
ASDisplayNodeAssertMainThread();
[_dataController reloadRowsAtIndexPaths:indexPaths withAnimationOptions:animation];
}
- (void)moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath
{
ASDisplayNodeAssertMainThread();
[_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 Intercepted selectors
@@ -499,17 +571,23 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)rangeControllerBeginUpdates:(ASRangeController *)rangeController
{
ASDisplayNodeAssertMainThread();
LOG(@"--- UITableView beginUpdates");
if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
[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();
LOG(@"--- UITableView endUpdates");
if (!self.asyncDataSource) {
if (completion) {
@@ -518,7 +596,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
}
[super endUpdates];
if (_automaticallyAdjustsContentOffset) {
[self endAdjustingContentOffset];
}
ASPerformBlockWithoutAnimation(!animated, ^{
[super endUpdates];
});
if (completion) {
completion(YES);
@@ -586,9 +670,10 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
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();
LOG(@"UITableView insertRows:%ld rows", indexPaths.count);
if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -598,11 +683,16 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
ASPerformBlockWithoutAnimation(preventAnimation, ^{
[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();
LOG(@"UITableView deleteRows:%ld rows", indexPaths.count);
if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -612,11 +702,17 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
ASPerformBlockWithoutAnimation(preventAnimation, ^{
[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
{
ASDisplayNodeAssertMainThread();
LOG(@"UITableView insertSections:%@", indexSet);
if (!self.asyncDataSource) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
@@ -631,6 +727,7 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
- (void)rangeController:(ASRangeController *)rangeController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
{
ASDisplayNodeAssertMainThread();
LOG(@"UITableView deleteSections:%@", indexSet);
if (!self.asyncDataSource) {
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.
*/
- (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.
@@ -75,7 +75,7 @@ typedef NSUInteger ASDataControllerAnimationOptions;
/**
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.
@@ -138,7 +138,7 @@ typedef NSUInteger ASDataControllerAnimationOptions;
- (void)endUpdates;
- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion;
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion;
- (void)insertSections:(NSIndexSet *)sections withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions;

View File

@@ -16,6 +16,9 @@
#import "ASMultidimensionalArrayUtils.h"
#import "ASDisplayNodeInternal.h"
//#define LOG(...) NSLog(__VA_ARGS__)
#define LOG(...)
const static NSUInteger kASDataControllerSizingCountPerProcessor = 5;
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.
_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:)];
_delegateDidDeleteSections = [_delegate respondsToSelector:@selector(dataController:didDeleteSectionsAtIndexSet:withAnimationOptions:)];
}
@@ -164,12 +167,14 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
if (indexPaths.count == 0)
return;
LOG(@"_deleteNodesAtIndexPaths:%@, full index paths in _editingNodes = %@", indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes));
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths);
ASDisplayNodePerformBlockOnMainThread(^{
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths);
ASDeleteElementsInMultidimensionalArrayAtIndexPaths(_completedNodes, indexPaths);
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];
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - reloadData");
// Remove everything that existed before the reload, now that we're ready to insert replacements
NSArray *indexPaths = ASIndexPathsForMultidimensionalArray(_editingNodes);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -318,32 +325,46 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
- (void)beginUpdates
{
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
// 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
{
[self endUpdatesWithCompletion:NULL];
[self endUpdatesAnimated:YES completion:nil];
}
- (void)endUpdatesWithCompletion:(void (^)(BOOL))completion
- (void)endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion
{
_batchUpdateCounter--;
if (_batchUpdateCounter == 0) {
[_delegate dataControllerBeginUpdates:self];
LOG(@"endUpdatesWithCompletion - beginning");
[_editingTransactionQueue addOperationWithBlock:^{
ASDisplayNodePerformBlockOnMainThread(^{
LOG(@"endUpdatesWithCompletion - begin updates call to delegate");
[_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.
LOG(@"endUpdatesWithCompletion - %zd blocks to run", _pendingEditCommandBlocks.count);
[_pendingEditCommandBlocks enumerateObjectsUsingBlock:^(dispatch_block_t block, NSUInteger idx, BOOL *stop) {
LOG(@"endUpdatesWithCompletion - running block #%zd", idx);
block();
}];
[_pendingEditCommandBlocks removeAllObjects];
[_delegate dataControllerEndUpdates:self completion:completion];
[_editingTransactionQueue addOperationWithBlock:^{
ASDisplayNodePerformBlockOnMainThread(^{
LOG(@"endUpdatesWithCompletion - calling delegate end");
[_delegate dataController:self endUpdatesAnimated:animated completion:completion];
});
}];
}
}
@@ -364,6 +385,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - insertSections: %@", indexSet);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{
@@ -372,6 +394,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[self _populateFromDataSourceWithSectionIndexSet:indexSet mutableNodes:updatedNodes mutableIndexPaths:updatedIndexPaths];
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - insertSections: %@", indexSet);
NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:indexSet.count];
for (NSUInteger i = 0; i < indexSet.count; i++) {
[sectionArray addObject:[NSMutableArray array]];
@@ -388,10 +411,12 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - deleteSections: %@", indexSet);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{
// remove elements
LOG(@"Edit Transaction - deleteSections: %@", indexSet);
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, indexSet);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -404,6 +429,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - reloadSections: %@", sections);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{
@@ -417,6 +444,9 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
[_editingTransactionQueue addOperationWithBlock:^{
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, sections);
LOG(@"Edit Transaction - reloadSections: updatedIndexPaths: %@, indexPaths: %@, _editingNodes: %@", updatedIndexPaths, indexPaths, ASIndexPathsForMultidimensionalArray(_editingNodes));
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
// reinsert the elements
@@ -430,10 +460,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - moveSection");
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{
// remove elements
LOG(@"Edit Transaction - moveSection");
NSArray *indexPaths = ASIndexPathsForMultidimensionalArrayAtIndexSet(_editingNodes, [NSIndexSet indexSetWithIndex:section]);
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, indexPaths);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
@@ -457,6 +492,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - insertRows: %@", indexPaths);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[self accessDataSourceWithBlock:^{
@@ -468,6 +505,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
}
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - insertRows: %@", indexPaths);
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
}];
@@ -478,12 +516,15 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - deleteRows: %@", indexPaths);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
// sort indexPath in order to avoid messing up the index when deleting
NSArray *sortedIndexPaths = [indexPaths sortedArrayUsingSelector:@selector(compare:)];
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - deleteRows: %@", indexPaths);
[self _deleteNodesAtIndexPaths:sortedIndexPaths withAnimationOptions:animationOptions];
}];
}];
@@ -493,6 +534,8 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - reloadRows: %@", indexPaths);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
// Reloading requires re-fetching the data. Load it on the current calling thread, locking the data source.
@@ -504,6 +547,7 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
}];
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - reloadRows: %@", indexPaths);
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
[self _batchLayoutNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
}];
@@ -515,9 +559,11 @@ static void *kASSizingQueueContext = &kASSizingQueueContext;
{
[self performEditCommandWithBlock:^{
ASDisplayNodeAssertMainThread();
LOG(@"Edit Command - moveRow: %@ > %@", indexPath, newIndexPath);
[_editingTransactionQueue waitUntilAllOperationsAreFinished];
[_editingTransactionQueue addOperationWithBlock:^{
LOG(@"Edit Transaction - moveRow: %@ > %@", indexPath, newIndexPath);
NSArray *nodes = ASFindElementsInMultidimensionalArrayAtIndexPaths(_editingNodes, [NSArray arrayWithObject:indexPath]);
NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
[self _deleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];

View File

@@ -103,16 +103,18 @@ static const CGFloat kASFlowLayoutControllerRefreshingThreshold = 0.3;
NSMutableSet *indexPathSet = [[NSMutableSet alloc] init];
NSArray *completedNodes = [_dataSource completedNodes];
ASIndexPath currPath = startPath;
while (!ASIndexPathEqualToIndexPath(startPath, endPath)) {
[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.
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");
while (currPath.row >= [(NSArray *)completedNodes[currPath.section] count] && currPath.section < completedNodes.count - 1) {
currPath.row = 0;
currPath.section++;
ASDisplayNodeAssert(currPath.section <= endPath.section, @"currPath should never reach a further section than endPath");
}
}

View File

@@ -88,7 +88,7 @@
*
* @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.
@@ -108,7 +108,7 @@
*
* @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.
@@ -119,7 +119,7 @@
*
* @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.

View File

@@ -89,6 +89,12 @@
}
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];
CGSize viewportSize = [_delegate rangeControllerViewportSize:self];
@@ -104,7 +110,7 @@
// this delegate decide what happens when a node is added or removed from a range
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];
// 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(^{
[_delegate rangeControllerEndUpdates:self completion:completion];
[_delegate rangeController:self endUpdatesAnimated:animated completion:completion];
});
}
@@ -192,14 +198,14 @@
ASDisplayNodePerformBlockOnMainThread(^{
_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(^{
_rangeIsValid = NO;
[_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOptions:animationOptions];
[_delegate rangeController:self didDeleteNodes:nodes atIndexPaths:indexPaths withAnimationOptions:animationOptions];
});
}