diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index ae1f579232..784ef0b4a4 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -703,6 +703,19 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; #pragma mark - #pragma mark Batch Fetching +- (void)_checkForBatchFetching +{ + // Dragging will be handled in scrollViewWillEndDragging:withVelocity:targetContentOffset: + if ([self isDragging] || [self isTracking] || ![self _shouldBatchFetch]) { + return; + } + + // Check if we should batch fetch + if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollableDirections], self.bounds, self.contentSize, self.contentOffset, _leadingScreensForBatching)) { + [self _beginBatchFetching]; + } +} + - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { _deceleratingVelocity = CGPointMake( @@ -711,7 +724,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; ); if (targetContentOffset != NULL) { - [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + [self _handleBatchFetchScrollingToOffset:*targetContentOffset]; } if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { @@ -738,7 +751,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } -- (BOOL)shouldBatchFetch +- (BOOL)_shouldBatchFetch { // if the delegate does not respond to this method, there is no point in starting to fetch BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)]; @@ -749,22 +762,27 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } -- (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset +- (void)_handleBatchFetchScrollingToOffset:(CGPoint)targetOffset { ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); - if (![self shouldBatchFetch]) { + if (![self _shouldBatchFetch]) { return; } if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { - [_batchContext beginBatchFetching]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext]; - }); + [self _beginBatchFetching]; } } +- (void)_beginBatchFetching +{ + [_batchContext beginBatchFetching]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext]; + }); +} + #pragma mark - ASDataControllerSource @@ -975,9 +993,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; }]; } else { [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; - [UIView performWithoutAnimation:^{ + ASPerformBlockWithoutAnimationCompletion(YES, ^{ [super insertItemsAtIndexPaths:indexPaths]; - }]; + }, ^{ + + }); } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index bf9a352209..9a6e7b771a 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -587,18 +587,46 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } else { scrollVelocity = _deceleratingVelocity; } + ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity]; return ASScrollDirectionApplyTransform(scrollDirection, self.transform); } -- (ASScrollDirection)_scrollDirectionForVelocity:(CGPoint)velocity +- (ASScrollDirection)scrollableDirections +{ + ASScrollDirection scrollableDirection = ASScrollDirectionNone; + CGFloat totalContentWidth = self.contentSize.width + self.contentInset.left + self.contentInset.right; + CGFloat totalContentHeight = self.contentSize.height + self.contentInset.top + self.contentInset.bottom; + + if (self.alwaysBounceHorizontal || totalContentWidth > self.bounds.size.width) { // Can scroll horizontally. + scrollableDirection |= ASScrollDirectionHorizontalDirections; + } + if (self.alwaysBounceVertical || totalContentHeight > self.bounds.size.height) { // Can scroll vertically. + scrollableDirection |= ASScrollDirectionVerticalDirections; + } + return scrollableDirection; +} + +- (ASScrollDirection)_scrollDirectionForVelocity:(CGPoint)scrollVelocity { ASScrollDirection direction = ASScrollDirectionNone; - if (velocity.y < 0.0) { - direction = ASScrollDirectionDown; - } else if (velocity.y > 0.0) { - direction = ASScrollDirectionUp; + ASScrollDirection scrollableDirections = [self scrollableDirections]; + + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { // Can scroll horizontally. + if (scrollVelocity.x < 0.0) { + direction |= ASScrollDirectionRight; + } else if (scrollVelocity.x > 0.0) { + direction |= ASScrollDirectionLeft; + } } + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) { // Can scroll vertically. + if (scrollVelocity.y < 0.0) { + direction |= ASScrollDirectionDown; + } else if (scrollVelocity.y > 0.0) { + direction |= ASScrollDirectionUp; + } + } + return direction; } @@ -631,7 +659,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; if (cellNode.neverShowPlaceholders) { [cellNode recursivelyEnsureDisplaySynchronously:YES]; @@ -650,7 +678,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASCellNode *cellNode = [cell node]; - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]) { ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); @@ -675,6 +703,19 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; #pragma mark - #pragma mark Batch Fetching +- (void)_checkForBatchFetching +{ + // Dragging will be handled in scrollViewWillEndDragging:withVelocity:targetContentOffset: + if ([self isDragging] || [self isTracking] || ![self _shouldBatchFetch]) { + return; + } + + // Check if we should batch fetch + if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollableDirections], self.bounds, self.contentSize, self.contentOffset, _leadingScreensForBatching)) { + [self _beginBatchFetching]; + } +} + - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { _deceleratingVelocity = CGPointMake( @@ -683,7 +724,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ); if (targetContentOffset != NULL) { - [self handleBatchFetchScrollingToOffset:*targetContentOffset]; + [self _handleBatchFetchScrollingToOffset:*targetContentOffset]; } if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:)]) { @@ -691,7 +732,20 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } -- (BOOL)shouldBatchFetch +- (void)_handleBatchFetchScrollingToOffset:(CGPoint)targetOffset +{ + ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); + + if (![self _shouldBatchFetch]) { + return; + } + + if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { + [self _beginBatchFetching]; + } +} + +- (BOOL)_shouldBatchFetch { // if the delegate does not respond to this method, there is no point in starting to fetch BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; @@ -702,20 +756,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } -- (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset +- (void)_beginBatchFetching { - ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); - - if (![self shouldBatchFetch]) { - return; - } - - if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { - [_batchContext beginBatchFetching]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - [_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext]; - }); - } + [_batchContext beginBatchFetching]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext]; + }); } #pragma mark - ASRangeControllerDataSource @@ -853,10 +899,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; if (!self.asyncDataSource) { return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes } - + BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; - ASPerformBlockWithoutAnimation(preventAnimation, ^{ + ASPerformBlockWithoutAnimationCompletion(preventAnimation, ^{ [super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; + }, ^{ + // Push this to the next runloop to be sure the UITableView has the right content size + dispatch_async(dispatch_get_main_queue(), ^{ + [self _checkForBatchFetching]; + }); }); if (_automaticallyAdjustsContentOffset) { @@ -874,8 +925,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; - ASPerformBlockWithoutAnimation(preventAnimation, ^{ + ASPerformBlockWithoutAnimationCompletion(preventAnimation, ^{ [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; + }, ^{ + // Push this to the next runloop to be sure the UITableView has the right content size + dispatch_async(dispatch_get_main_queue(), ^{ + [self _checkForBatchFetching]; + }); }); if (_automaticallyAdjustsContentOffset) { @@ -1077,7 +1133,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their // their update in the layout pass if (![node supportsRangeManagedInterfaceState]) { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; } } diff --git a/AsyncDisplayKit/Details/ASBatchContext.h b/AsyncDisplayKit/Details/ASBatchContext.h index dc1986a792..aace1facd2 100644 --- a/AsyncDisplayKit/Details/ASBatchContext.h +++ b/AsyncDisplayKit/Details/ASBatchContext.h @@ -33,6 +33,14 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)completeBatchFetching:(BOOL)didComplete; +/** + * Let the context object know that a batch fetch was completed. + * + * @discussion For instance, when a table has reached the end of its data, a batch fetch will be attempted unless the context + * object thinks that it is still fetching. + */ +- (void)completeBatchFetching; + /** * Ask the context object if the batch fetching process was cancelled by the context owner. * diff --git a/AsyncDisplayKit/Details/ASBatchContext.mm b/AsyncDisplayKit/Details/ASBatchContext.mm index 4833cbd826..f86449d6e4 100644 --- a/AsyncDisplayKit/Details/ASBatchContext.mm +++ b/AsyncDisplayKit/Details/ASBatchContext.mm @@ -45,24 +45,31 @@ typedef NS_ENUM(NSInteger, ASBatchContextState) { return _state == ASBatchContextStateCancelled; } -- (void)completeBatchFetching:(BOOL)didComplete -{ - if (didComplete) { - ASDN::MutexLocker l(_propertyLock); - _state = ASBatchContextStateCompleted; - } -} - - (void)beginBatchFetching { ASDN::MutexLocker l(_propertyLock); _state = ASBatchContextStateFetching; } +- (void)completeBatchFetching +{ + ASDN::MutexLocker l(_propertyLock); + _state = ASBatchContextStateCompleted; +} + - (void)cancelBatchFetching { ASDN::MutexLocker l(_propertyLock); _state = ASBatchContextStateCancelled; } +#pragma mark - Deprecated + +- (void)completeBatchFetching:(BOOL)didComplete +{ + if (didComplete) { + [self completeBatchFetching]; + } +} + @end diff --git a/AsyncDisplayKit/Private/ASBatchFetching.m b/AsyncDisplayKit/Private/ASBatchFetching.m index b2a471e2da..f25408e335 100644 --- a/AsyncDisplayKit/Private/ASBatchFetching.m +++ b/AsyncDisplayKit/Private/ASBatchFetching.m @@ -20,7 +20,7 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, } // only Down and Right scrolls are currently supported (tail loading) - if (scrollDirection != ASScrollDirectionDown && scrollDirection != ASScrollDirectionRight) { + if (!ASScrollDirectionContainsDown(scrollDirection) && !ASScrollDirectionContainsRight(scrollDirection)) { return NO; } @@ -31,11 +31,11 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context, CGFloat viewLength, offset, contentLength; - if (scrollDirection == ASScrollDirectionDown) { + if (ASScrollDirectionContainsDown(scrollDirection)) { viewLength = bounds.size.height; offset = targetOffset.y; contentLength = contentSize.height; - } else { // horizontal + } else { // horizontal / right viewLength = bounds.size.width; offset = targetOffset.x; contentLength = contentSize.width; diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h index 79f500e77c..8cfc7713de 100644 --- a/AsyncDisplayKit/Private/ASInternalHelpers.h +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -32,6 +32,26 @@ BOOL ASRunningOnOS7(); ASDISPLAYNODE_EXTERN_C_END +/** + @summary Conditionally performs UIView geometry changes in the given block without animation and call completion block afterwards. + + Used primarily to circumvent UITableView forcing insertion animations when explicitly told not to via + `UITableViewRowAnimationNone`. More info: https://github.com/facebook/AsyncDisplayKit/pull/445 + + @param withoutAnimation Set to `YES` to perform given block without animation + @param block Perform UIView geometry changes within the passed block + @param completion Call completion block if UIView geometry changes within the passed block did complete + */ +ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimationCompletion(BOOL withoutAnimation, void (^block)(), void (^completion)()) { + [CATransaction begin]; + [CATransaction setDisableActions:withoutAnimation]; + if (completion != nil) { + [CATransaction setCompletionBlock:completion]; + } + block(); + [CATransaction commit]; +} + /** @summary Conditionally performs UIView geometry changes in the given block without animation. @@ -42,11 +62,7 @@ ASDISPLAYNODE_EXTERN_C_END @param block Perform UIView geometry changes within the passed block */ ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { - if (withoutAnimation) { - [UIView performWithoutAnimation:block]; - } else { - block(); - } + ASPerformBlockWithoutAnimationCompletion(withoutAnimation, block, nil); } ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origin, CGPoint anchorPoint, CGRect *bounds, CGPoint *position) diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index 9ec66fa418..699e17fd7f 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -177,7 +177,7 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell [_kittenDataSource addObjectsFromArray:moarKittens]; [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; - [context completeBatchFetching:YES]; + [context completeBatchFetching]; }); }