Fix fetch call won't occur for content smaller than bounds unless user scrolls

This commit is contained in:
Michael Schneider 2016-04-04 20:51:24 -07:00
parent 0fbc77dd25
commit c25a252e1c
7 changed files with 160 additions and 53 deletions

View File

@ -703,6 +703,19 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
#pragma mark - #pragma mark -
#pragma mark Batch Fetching #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 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ {
_deceleratingVelocity = CGPointMake( _deceleratingVelocity = CGPointMake(
@ -711,7 +724,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
); );
if (targetContentOffset != NULL) { if (targetContentOffset != NULL) {
[self handleBatchFetchScrollingToOffset:*targetContentOffset]; [self _handleBatchFetchScrollingToOffset:*targetContentOffset];
} }
if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity: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 // if the delegate does not respond to this method, there is no point in starting to fetch
BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)]; BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(collectionView:willBeginBatchFetchWithContext:)];
@ -749,20 +762,25 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
} }
} }
- (void)handleBatchFetchScrollingToOffset:(CGPoint)targetOffset - (void)_handleBatchFetchScrollingToOffset:(CGPoint)targetOffset
{ {
ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist"); ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist");
if (![self shouldBatchFetch]) { if (![self _shouldBatchFetch]) {
return; return;
} }
if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) { if (ASDisplayShouldFetchBatchForContext(_batchContext, [self scrollDirection], self.bounds, self.contentSize, targetOffset, _leadingScreensForBatching)) {
[self _beginBatchFetching];
}
}
- (void)_beginBatchFetching
{
[_batchContext beginBatchFetching]; [_batchContext beginBatchFetching];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext]; [_asyncDelegate collectionView:self willBeginBatchFetchWithContext:_batchContext];
}); });
}
} }
@ -975,9 +993,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
}]; }];
} else { } else {
[_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO];
[UIView performWithoutAnimation:^{ ASPerformBlockWithoutAnimationCompletion(YES, ^{
[super insertItemsAtIndexPaths:indexPaths]; [super insertItemsAtIndexPaths:indexPaths];
}]; }, ^{
});
} }
} }

View File

@ -587,18 +587,46 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
} else { } else {
scrollVelocity = _deceleratingVelocity; scrollVelocity = _deceleratingVelocity;
} }
ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity]; ASScrollDirection scrollDirection = [self _scrollDirectionForVelocity:scrollVelocity];
return ASScrollDirectionApplyTransform(scrollDirection, self.transform); 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; ASScrollDirection direction = ASScrollDirectionNone;
if (velocity.y < 0.0) { ASScrollDirection scrollableDirections = [self scrollableDirections];
direction = ASScrollDirectionDown;
} else if (velocity.y > 0.0) { if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections)) { // Can scroll horizontally.
direction = ASScrollDirectionUp; 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; return direction;
} }
@ -631,7 +659,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
[_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath];
} }
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]];
if (cellNode.neverShowPlaceholders) { if (cellNode.neverShowPlaceholders) {
[cellNode recursivelyEnsureDisplaySynchronously:YES]; [cellNode recursivelyEnsureDisplaySynchronously:YES];
@ -650,7 +678,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
ASCellNode *cellNode = [cell node]; ASCellNode *cellNode = [cell node];
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]];
if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]) { if ([_asyncDelegate respondsToSelector:@selector(tableView:didEndDisplayingNode:forRowAtIndexPath:)]) {
ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); 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 -
#pragma mark Batch Fetching #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 - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{ {
_deceleratingVelocity = CGPointMake( _deceleratingVelocity = CGPointMake(
@ -683,7 +724,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
); );
if (targetContentOffset != NULL) { if (targetContentOffset != NULL) {
[self handleBatchFetchScrollingToOffset:*targetContentOffset]; [self _handleBatchFetchScrollingToOffset:*targetContentOffset];
} }
if ([_asyncDelegate respondsToSelector:@selector(scrollViewWillEndDragging:withVelocity: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 // if the delegate does not respond to this method, there is no point in starting to fetch
BOOL canFetch = [_asyncDelegate respondsToSelector:@selector(tableView:willBeginBatchFetchWithContext:)]; 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]; [_batchContext beginBatchFetching];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext]; [_asyncDelegate tableView:self willBeginBatchFetchWithContext:_batchContext];
}); });
}
} }
#pragma mark - ASRangeControllerDataSource #pragma mark - ASRangeControllerDataSource
@ -855,8 +901,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
} }
BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone;
ASPerformBlockWithoutAnimation(preventAnimation, ^{ ASPerformBlockWithoutAnimationCompletion(preventAnimation, ^{
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [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) { if (_automaticallyAdjustsContentOffset) {
@ -874,8 +925,13 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
} }
BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone; BOOL preventAnimation = animationOptions == UITableViewRowAnimationNone;
ASPerformBlockWithoutAnimation(preventAnimation, ^{ ASPerformBlockWithoutAnimationCompletion(preventAnimation, ^{
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [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) { 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 // Updating the visible node index paths only for not range managed nodes. Range managed nodes will get their
// their update in the layout pass // their update in the layout pass
if (![node supportsRangeManagedInterfaceState]) { if (![node supportsRangeManagedInterfaceState]) {
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]];
} }
} }

View File

@ -33,6 +33,14 @@ NS_ASSUME_NONNULL_BEGIN
*/ */
- (void)completeBatchFetching:(BOOL)didComplete; - (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. * Ask the context object if the batch fetching process was cancelled by the context owner.
* *

View File

@ -45,24 +45,31 @@ typedef NS_ENUM(NSInteger, ASBatchContextState) {
return _state == ASBatchContextStateCancelled; return _state == ASBatchContextStateCancelled;
} }
- (void)completeBatchFetching:(BOOL)didComplete
{
if (didComplete) {
ASDN::MutexLocker l(_propertyLock);
_state = ASBatchContextStateCompleted;
}
}
- (void)beginBatchFetching - (void)beginBatchFetching
{ {
ASDN::MutexLocker l(_propertyLock); ASDN::MutexLocker l(_propertyLock);
_state = ASBatchContextStateFetching; _state = ASBatchContextStateFetching;
} }
- (void)completeBatchFetching
{
ASDN::MutexLocker l(_propertyLock);
_state = ASBatchContextStateCompleted;
}
- (void)cancelBatchFetching - (void)cancelBatchFetching
{ {
ASDN::MutexLocker l(_propertyLock); ASDN::MutexLocker l(_propertyLock);
_state = ASBatchContextStateCancelled; _state = ASBatchContextStateCancelled;
} }
#pragma mark - Deprecated
- (void)completeBatchFetching:(BOOL)didComplete
{
if (didComplete) {
[self completeBatchFetching];
}
}
@end @end

View File

@ -20,7 +20,7 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context,
} }
// only Down and Right scrolls are currently supported (tail loading) // only Down and Right scrolls are currently supported (tail loading)
if (scrollDirection != ASScrollDirectionDown && scrollDirection != ASScrollDirectionRight) { if (!ASScrollDirectionContainsDown(scrollDirection) && !ASScrollDirectionContainsRight(scrollDirection)) {
return NO; return NO;
} }
@ -31,11 +31,11 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context,
CGFloat viewLength, offset, contentLength; CGFloat viewLength, offset, contentLength;
if (scrollDirection == ASScrollDirectionDown) { if (ASScrollDirectionContainsDown(scrollDirection)) {
viewLength = bounds.size.height; viewLength = bounds.size.height;
offset = targetOffset.y; offset = targetOffset.y;
contentLength = contentSize.height; contentLength = contentSize.height;
} else { // horizontal } else { // horizontal / right
viewLength = bounds.size.width; viewLength = bounds.size.width;
offset = targetOffset.x; offset = targetOffset.x;
contentLength = contentSize.width; contentLength = contentSize.width;

View File

@ -32,6 +32,26 @@ BOOL ASRunningOnOS7();
ASDISPLAYNODE_EXTERN_C_END 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. @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 @param block Perform UIView geometry changes within the passed block
*/ */
ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) { ASDISPLAYNODE_INLINE void ASPerformBlockWithoutAnimation(BOOL withoutAnimation, void (^block)()) {
if (withoutAnimation) { ASPerformBlockWithoutAnimationCompletion(withoutAnimation, block, nil);
[UIView performWithoutAnimation:block];
} else {
block();
}
} }
ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origin, CGPoint anchorPoint, CGRect *bounds, CGPoint *position) ASDISPLAYNODE_INLINE void ASBoundsAndPositionForFrame(CGRect rect, CGPoint origin, CGPoint anchorPoint, CGRect *bounds, CGPoint *position)

View File

@ -177,7 +177,7 @@ static const NSInteger kMaxLitterSize = 100; // max number of kitten cell
[_kittenDataSource addObjectsFromArray:moarKittens]; [_kittenDataSource addObjectsFromArray:moarKittens];
[tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade]; [tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
[context completeBatchFetching:YES]; [context completeBatchFetching];
}); });
} }