diff --git a/AsyncDisplayKit/ASCollectionView.h b/AsyncDisplayKit/ASCollectionView.h index ed2526ff95..575f22bb71 100644 --- a/AsyncDisplayKit/ASCollectionView.h +++ b/AsyncDisplayKit/ASCollectionView.h @@ -78,6 +78,30 @@ */ @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. * diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index ee9648cdc7..29260aa86a 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -281,11 +281,16 @@ 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 { [_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 @@ -540,7 +545,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 +555,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 +596,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 +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(); diff --git a/AsyncDisplayKit/ASTableView.h b/AsyncDisplayKit/ASTableView.h index f8d9a60f7d..816a41f0a9 100644 --- a/AsyncDisplayKit/ASTableView.h +++ b/AsyncDisplayKit/ASTableView.h @@ -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. Batches are asynchronous an thread safe. */ - (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; +/** + * 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. * @@ -222,6 +247,14 @@ */ - (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 diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index bfaded25ab..9645f30e9e 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -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 @@ -326,9 +333,13 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - (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 Editing @@ -373,6 +384,57 @@ void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { [_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 %@", 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]; diff --git a/AsyncDisplayKit/Details/ASFlowLayoutController.mm b/AsyncDisplayKit/Details/ASFlowLayoutController.mm index 52b9e6e880..7fce4c636d 100644 --- a/AsyncDisplayKit/Details/ASFlowLayoutController.mm +++ b/AsyncDisplayKit/Details/ASFlowLayoutController.mm @@ -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"); } } diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index d8dd1bf58e..2473f302b2 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -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. diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index f41d9d291c..b22db853bf 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -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 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]; }); }