Skip to content

Commit df497b8

Browse files
Adlai Hollerappleguy
authored andcommitted
Improve Batch Fetching Call Pattern to Reduce Fetch Calls (#2675)
* Test & tweak batch fetching implementation * Tighten the test * Stop batch fetching at the end of range controller pass * Clean up the test * Still check for batch fetching after each frame when scrolling * Ensure batch fetching happens for empty collection/table
1 parent aa9dff0 commit df497b8

File tree

8 files changed

+143
-55
lines changed

8 files changed

+143
-55
lines changed

AsyncDisplayKit/ASCollectionView.mm

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ @interface ASCollectionView () <ASRangeControllerDataSource, ASRangeControllerDe
174174
* The collection view never queried your data source before the update to see that it actually had 0 items.
175175
*/
176176
BOOL _superIsPendingDataLoad;
177+
178+
/**
179+
* It's important that we always check for batch fetching at least once, but also
180+
* that we do not check for batch fetching for empty updates (as that may cause an infinite
181+
* loop of batch fetching, where the batch completes and performBatchUpdates: is called without
182+
* actually making any changes.) So to handle the case where a collection is completely empty
183+
* (0 sections) we always check at least once after each update (initial reload is the first update.)
184+
*/
185+
BOOL _hasEverCheckedForBatchFetchingDueToUpdate;
177186

178187
struct {
179188
unsigned int scrollViewDidScroll:1;
@@ -1080,6 +1089,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
10801089
ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController];
10811090
if (ASInterfaceStateIncludesVisible(interfaceState)) {
10821091
[_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull];
1092+
[self _checkForBatchFetching];
10831093
}
10841094

10851095
for (_ASCollectionViewCell *collectionCell in _cellsForVisibilityUpdates) {
@@ -1103,7 +1113,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi
11031113

11041114
if (targetContentOffset != NULL) {
11051115
ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist");
1106-
[self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollDirection] contentOffset:*targetContentOffset];
1116+
[self _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset];
11071117
}
11081118

11091119
if (_asyncDelegateFlags.scrollViewWillEndDragging) {
@@ -1244,9 +1254,10 @@ - (BOOL)canBatchFetch
12441254
- (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes
12451255
{
12461256
// Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided
1247-
if (changes == 0) {
1257+
if (changes == 0 && _hasEverCheckedForBatchFetchingDueToUpdate) {
12481258
return;
12491259
}
1260+
_hasEverCheckedForBatchFetchingDueToUpdate = YES;
12501261

12511262
// Push this to the next runloop to be sure the scroll view has the right content size
12521263
dispatch_async(dispatch_get_main_queue(), ^{
@@ -1261,12 +1272,12 @@ - (void)_checkForBatchFetching
12611272
return;
12621273
}
12631274

1264-
[self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollableDirections] contentOffset:self.contentOffset];
1275+
[self _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset];
12651276
}
12661277

1267-
- (void)_beginBatchFetchingIfNeededWithScrollView:(UIScrollView<ASBatchFetchingScrollView> *)scrollView forScrollDirection:(ASScrollDirection)scrollDirection contentOffset:(CGPoint)contentOffset
1278+
- (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset
12681279
{
1269-
if (ASDisplayShouldFetchBatchForScrollView(self, scrollDirection, contentOffset)) {
1280+
if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, self.scrollableDirections, contentOffset)) {
12701281
[self _beginBatchFetching];
12711282
}
12721283
}
@@ -1521,11 +1532,6 @@ - (void)rangeController:(ASRangeController *)rangeController didEndUpdatesAnimat
15211532
_performingBatchUpdates = NO;
15221533
}
15231534

1524-
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController
1525-
{
1526-
[self _checkForBatchFetching];
1527-
}
1528-
15291535
- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
15301536
{
15311537
ASDisplayNodeAssertMainThread();

AsyncDisplayKit/ASTableView.mm

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ @interface ASTableView () <ASRangeControllerDataSource, ASRangeControllerDelegat
140140
BOOL _performingBatchUpdates;
141141
NSMutableSet *_cellsForVisibilityUpdates;
142142

143+
// See documentation on same property in ASCollectionView
144+
BOOL _hasEverCheckedForBatchFetchingDueToUpdate;
145+
143146
// The section index overlay view, if there is one present.
144147
// This is useful because we need to measure our row nodes against (width - indexView.width).
145148
__weak UIView *_sectionIndexView;
@@ -1040,6 +1043,7 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
10401043
ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController];
10411044
if (ASInterfaceStateIncludesVisible(interfaceState)) {
10421045
[_rangeController updateCurrentRangeWithMode:ASLayoutRangeModeFull];
1046+
[self _checkForBatchFetching];
10431047
}
10441048

10451049
for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) {
@@ -1062,7 +1066,7 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoi
10621066

10631067
if (targetContentOffset != NULL) {
10641068
ASDisplayNodeAssert(_batchContext != nil, @"Batch context should exist");
1065-
[self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollDirection] contentOffset:*targetContentOffset];
1069+
[self _beginBatchFetchingIfNeededWithContentOffset:*targetContentOffset];
10661070
}
10671071

10681072
if (_asyncDelegateFlags.scrollViewWillEndDragging) {
@@ -1168,9 +1172,10 @@ - (BOOL)canBatchFetch
11681172
- (void)_scheduleCheckForBatchFetchingForNumberOfChanges:(NSUInteger)changes
11691173
{
11701174
// Prevent fetching will continually trigger in a loop after reaching end of content and no new content was provided
1171-
if (changes == 0) {
1175+
if (changes == 0 && _hasEverCheckedForBatchFetchingDueToUpdate) {
11721176
return;
11731177
}
1178+
_hasEverCheckedForBatchFetchingDueToUpdate = YES;
11741179

11751180
// Push this to the next runloop to be sure the scroll view has the right content size
11761181
dispatch_async(dispatch_get_main_queue(), ^{
@@ -1185,12 +1190,12 @@ - (void)_checkForBatchFetching
11851190
return;
11861191
}
11871192

1188-
[self _beginBatchFetchingIfNeededWithScrollView:self forScrollDirection:[self scrollableDirections] contentOffset:self.contentOffset];
1193+
[self _beginBatchFetchingIfNeededWithContentOffset:self.contentOffset];
11891194
}
11901195

1191-
- (void)_beginBatchFetchingIfNeededWithScrollView:(UIScrollView<ASBatchFetchingScrollView> *)scrollView forScrollDirection:(ASScrollDirection)scrollDirection contentOffset:(CGPoint)contentOffset
1196+
- (void)_beginBatchFetchingIfNeededWithContentOffset:(CGPoint)contentOffset
11921197
{
1193-
if (ASDisplayShouldFetchBatchForScrollView(self, scrollDirection, contentOffset)) {
1198+
if (ASDisplayShouldFetchBatchForScrollView(self, self.scrollDirection, ASScrollDirectionVerticalDirections, contentOffset)) {
11941199
[self _beginBatchFetching];
11951200
}
11961201
}
@@ -1329,11 +1334,6 @@ - (void)rangeController:(ASRangeController *)rangeController didEndUpdatesAnimat
13291334
}
13301335
}
13311336

1332-
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController
1333-
{
1334-
[self _checkForBatchFetching];
1335-
}
1336-
13371337
- (void)rangeController:(ASRangeController *)rangeController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOptions:(ASDataControllerAnimationOptions)animationOptions
13381338
{
13391339
ASDisplayNodeAssertMainThread();

AsyncDisplayKit/Details/ASRangeController.h

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,6 @@ NS_ASSUME_NONNULL_BEGIN
159159
*/
160160
- (void)rangeController:(ASRangeController * )rangeController didEndUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion;
161161

162-
/**
163-
* Completed updates to cell node addition and removal.
164-
*
165-
* @param rangeController Sender.
166-
*/
167-
- (void)didCompleteUpdatesInRangeController:(ASRangeController *)rangeController;
168-
169162
/**
170163
* Called for nodes insertion.
171164
*

AsyncDisplayKit/Details/ASRangeController.mm

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ - (void)_updateVisibleNodeIndexPaths
379379
[modifiedIndexPaths sortUsingSelector:@selector(compare:)];
380380
NSLog(@"Range update complete; modifiedIndexPaths: %@", [self descriptionWithIndexPaths:modifiedIndexPaths]);
381381
#endif
382-
[_delegate didCompleteUpdatesInRangeController:self];
383382

384383
ASProfilingSignpostEnd(1, self);
385384
}

AsyncDisplayKit/Private/ASBatchFetching.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,18 @@ ASDISPLAYNODE_EXTERN_C_BEGIN
2929
* ASCollectionView batch fetching API.
3030
@param context The scroll view that in-flight fetches are happening.
3131
@param scrollDirection The current scrolling direction of the scroll view.
32+
@param scrollableDirections The possible scrolling directions of the scroll view.
3233
@param targetOffset The offset that the scrollview will scroll to.
3334
@return Whether or not the current state should proceed with batch fetching.
3435
*/
35-
BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView<ASBatchFetchingScrollView> *scrollView, ASScrollDirection scrollDirection, CGPoint contentOffset);
36+
BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView<ASBatchFetchingScrollView> *scrollView, ASScrollDirection scrollDirection, ASScrollDirection scrollableDirections, CGPoint contentOffset);
3637

3738

3839
/**
3940
@abstract Determine if batch fetching should begin based on the state of the parameters.
4041
@param context The batch fetching context that contains knowledge about in-flight fetches.
4142
@param scrollDirection The current scrolling direction of the scroll view.
43+
@param scrollableDirections The possible scrolling directions of the scroll view.
4244
@param bounds The bounds of the scrollview.
4345
@param contentSize The content size of the scrollview.
4446
@param targetOffset The offset that the scrollview will scroll to.
@@ -49,6 +51,7 @@ BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView<ASBatchFetchingScrollVi
4951
*/
5052
extern BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context,
5153
ASScrollDirection scrollDirection,
54+
ASScrollDirection scrollableDirections,
5255
CGRect bounds,
5356
CGSize contentSize,
5457
CGPoint targetOffset,

AsyncDisplayKit/Private/ASBatchFetching.m

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
#import "ASBatchFetching.h"
1212

13-
BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView<ASBatchFetchingScrollView> *scrollView, ASScrollDirection scrollDirection, CGPoint contentOffset)
13+
BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView<ASBatchFetchingScrollView> *scrollView, ASScrollDirection scrollDirection, ASScrollDirection scrollableDirections, CGPoint contentOffset)
1414
{
1515
// Don't fetch if the scroll view does not allow
1616
if (![scrollView canBatchFetch]) {
@@ -22,11 +22,12 @@ BOOL ASDisplayShouldFetchBatchForScrollView(UIScrollView<ASBatchFetchingScrollVi
2222
CGRect bounds = scrollView.bounds;
2323
CGSize contentSize = scrollView.contentSize;
2424
CGFloat leadingScreens = scrollView.leadingScreensForBatching;
25-
return ASDisplayShouldFetchBatchForContext(context, scrollDirection, bounds, contentSize, contentOffset, leadingScreens);
25+
return ASDisplayShouldFetchBatchForContext(context, scrollDirection, scrollableDirections, bounds, contentSize, contentOffset, leadingScreens);
2626
}
2727

2828
BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context,
2929
ASScrollDirection scrollDirection,
30+
ASScrollDirection scrollableDirections,
3031
CGRect bounds,
3132
CGSize contentSize,
3233
CGPoint targetOffset,
@@ -37,19 +38,14 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context,
3738
return NO;
3839
}
3940

40-
// Only Down and Right scrolls are currently supported (tail loading)
41-
if (!ASScrollDirectionContainsDown(scrollDirection) && !ASScrollDirectionContainsRight(scrollDirection)) {
42-
return NO;
43-
}
44-
4541
// No fetching for null states
46-
if (leadingScreens <= 0.0 || CGRectEqualToRect(bounds, CGRectZero)) {
42+
if (leadingScreens <= 0.0 || CGRectIsEmpty(bounds)) {
4743
return NO;
4844
}
4945

5046
CGFloat viewLength, offset, contentLength;
5147

52-
if (ASScrollDirectionContainsDown(scrollDirection)) {
48+
if (ASScrollDirectionContainsVerticalDirection(scrollableDirections)) {
5349
viewLength = bounds.size.height;
5450
offset = targetOffset.y;
5551
contentLength = contentSize.height;
@@ -61,9 +57,14 @@ BOOL ASDisplayShouldFetchBatchForContext(ASBatchContext *context,
6157

6258
// target offset will always be 0 if the content size is smaller than the viewport
6359
BOOL hasSmallContent = offset == 0.0 && contentLength < viewLength;
60+
if (hasSmallContent) {
61+
return YES;
62+
}
63+
64+
BOOL isScrollingTowardEnd = (ASScrollDirectionContainsDown(scrollDirection) || ASScrollDirectionContainsRight(scrollDirection));
6465

6566
CGFloat triggerDistance = viewLength * leadingScreens;
6667
CGFloat remainingDistance = contentLength - viewLength - offset;
6768

68-
return hasSmallContent || remainingDistance <= triggerDistance;
69+
return isScrollingTowardEnd && remainingDistance <= triggerDistance;
6970
}

0 commit comments

Comments
 (0)