[ASRangeController] Update synchronously when possible

This commit is contained in:
Adlai Holler
2016-05-03 16:44:09 -07:00
committed by Adlai Holler
parent 2e3da9bc92
commit edb4e45c24
6 changed files with 196 additions and 121 deletions

View File

@@ -272,6 +272,10 @@
{ {
[super visibleStateDidChange:isVisible]; [super visibleStateDidChange:isVisible];
if (isVisible && self.neverShowPlaceholders) {
[self recursivelyEnsureDisplaySynchronously:YES];
}
// NOTE: This assertion is failing in some apps and will be enabled soon. // NOTE: This assertion is failing in some apps and will be enabled soon.
// ASDisplayNodeAssert(self.isNodeLoaded, @"Node should be loaded in order for it to become visible or invisible. If not in this situation, we shouldn't trigger creating the view."); // ASDisplayNodeAssert(self.isNodeLoaded, @"Node should be loaded in order for it to become visible or invisible. If not in this situation, we shouldn't trigger creating the view.");
UIView *view = self.view; UIView *view = self.view;

View File

@@ -639,11 +639,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
[_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath];
} }
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; [_rangeController setNeedsUpdate];
if (cellNode.neverShowPlaceholders) {
[cellNode recursivelyEnsureDisplaySynchronously:YES];
}
if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) {
[_cellsForVisibilityUpdates addObject:cell]; [_cellsForVisibilityUpdates addObject:cell];
} }
@@ -651,8 +648,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
{ {
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection];
ASCellNode *cellNode = [cell node]; ASCellNode *cellNode = [cell node];
if (_asyncDelegateFlags.asyncDelegateCollectionViewDidEndDisplayingNodeForItemAtIndexPath) { if (_asyncDelegateFlags.asyncDelegateCollectionViewDidEndDisplayingNodeForItemAtIndexPath) {
@@ -660,9 +655,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
[_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath];
} }
if ([_cellsForVisibilityUpdates containsObject:cell]) { [_rangeController setNeedsUpdate];
[_cellsForVisibilityUpdates removeObject:cell]; [_cellsForVisibilityUpdates removeObject:cell];
}
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -844,6 +839,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
// To ensure _maxSizeForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last // To ensure _maxSizeForNodesConstrainedSize is up-to-date for every usage, this call to super must be done last
[super layoutSubviews]; [super layoutSubviews];
// Update range controller immediately if possible & needed.
// Calling -updateIfNeeded in here with self.window == nil (early in the collection view's life)
// may cause UICollectionView data related crashes. We'll update in -didMoveToWindow anyway.
if (self.window != nil) {
[_rangeController updateIfNeeded];
}
} }
@@ -1030,13 +1032,17 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
- (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController - (NSArray *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
// Calling -indexPathsForVisibleItems will trigger UIKit to call reloadData if it never has, which can result
// Calling visibleNodeIndexPathsForRangeController: will trigger UIKit to call reloadData if it never has, which can result
// in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast. // in incorrect layout if performed at zero size. We can use the fact that nothing can be visible at zero size to return fast.
BOOL isZeroSized = CGRectEqualToRect(self.bounds, CGRectZero); BOOL isZeroSized = CGRectEqualToRect(self.bounds, CGRectZero);
return isZeroSized ? @[] : [self indexPathsForVisibleItems]; return isZeroSized ? @[] : [self indexPathsForVisibleItems];
} }
- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController
{
return self.scrollDirection;
}
- (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
@@ -1085,9 +1091,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
block(); block();
} }
} completion:^(BOOL finished){ } completion:^(BOOL finished){
// Flush any range changes that happened as part of the update animations ending.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdateBlocks]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdateBlocks];
if (completion) { completion(finished); } if (completion) { completion(finished); }
}]; }];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
}); });
[_batchUpdateBlocks removeAllObjects]; [_batchUpdateBlocks removeAllObjects];
@@ -1114,6 +1124,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
} else { } else {
[UIView performWithoutAnimation:^{ [UIView performWithoutAnimation:^{
[super insertItemsAtIndexPaths:indexPaths]; [super insertItemsAtIndexPaths:indexPaths];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count];
}]; }];
} }
@@ -1134,6 +1146,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
} else { } else {
[UIView performWithoutAnimation:^{ [UIView performWithoutAnimation:^{
[super deleteItemsAtIndexPaths:indexPaths]; [super deleteItemsAtIndexPaths:indexPaths];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count];
}]; }];
} }
@@ -1154,6 +1168,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
} else { } else {
[UIView performWithoutAnimation:^{ [UIView performWithoutAnimation:^{
[super insertSections:indexSet]; [super insertSections:indexSet];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count];
}]; }];
} }
@@ -1174,6 +1190,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
} else { } else {
[UIView performWithoutAnimation:^{ [UIView performWithoutAnimation:^{
[super deleteSections:indexSet]; [super deleteSections:indexSet];
// Flush any range changes that happened as part of submitting the update.
[_rangeController updateIfNeeded];
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count];
}]; }];
} }
@@ -1275,7 +1293,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
// 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 setNeedsUpdate];
[_rangeController updateIfNeeded];
} }
} }

View File

@@ -23,6 +23,7 @@
#import "ASLayout.h" #import "ASLayout.h"
#import "_ASDisplayLayer.h" #import "_ASDisplayLayer.h"
#import "ASTableNode.h" #import "ASTableNode.h"
#import "ASEqualityHelpers.h"
static const ASSizeRange kInvalidSizeRange = {CGSizeZero, CGSizeZero}; static const ASSizeRange kInvalidSizeRange = {CGSizeZero, CGSizeZero};
static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
@@ -126,6 +127,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
BOOL _ignoreNodesConstrainedWidthChange; BOOL _ignoreNodesConstrainedWidthChange;
BOOL _queuedNodeHeightUpdate; BOOL _queuedNodeHeightUpdate;
BOOL _isDeallocating; BOOL _isDeallocating;
BOOL _performingBatchUpdates;
NSMutableSet *_cellsForVisibilityUpdates; NSMutableSet *_cellsForVisibilityUpdates;
struct { struct {
@@ -468,6 +470,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
// To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last // To ensure _nodesConstrainedWidth is up-to-date for every usage, this call to super must be done last
[super layoutSubviews]; [super layoutSubviews];
[_rangeController updateIfNeeded];
} }
#pragma mark - #pragma mark -
@@ -644,11 +647,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
[_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath];
} }
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; [_rangeController setNeedsUpdate];
if (cellNode.neverShowPlaceholders) {
[cellNode recursivelyEnsureDisplaySynchronously:YES];
}
if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) {
[_cellsForVisibilityUpdates addObject:cell]; [_cellsForVisibilityUpdates addObject:cell];
@@ -657,22 +656,20 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(_ASTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{ {
if ([_pendingVisibleIndexPath isEqual:indexPath]) { if (ASObjectIsEqual(_pendingVisibleIndexPath, indexPath)) {
_pendingVisibleIndexPath = nil; _pendingVisibleIndexPath = nil;
} }
ASCellNode *cellNode = [cell node]; ASCellNode *cellNode = [cell node];
[_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; [_rangeController setNeedsUpdate];
if (_asyncDelegateFlags.asyncDelegateTableViewDidEndDisplayingNodeForRowAtIndexPath) { if (_asyncDelegateFlags.asyncDelegateTableViewDidEndDisplayingNodeForRowAtIndexPath) {
ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil.");
[_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath];
} }
if ([_cellsForVisibilityUpdates containsObject:cell]) {
[_cellsForVisibilityUpdates removeObject:cell]; [_cellsForVisibilityUpdates removeObject:cell];
}
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations" #pragma clang diagnostic ignored "-Wdeprecated-declarations"
@@ -866,61 +863,38 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
return @[]; return @[];
} }
// In this case we cannot use indexPathsForVisibleRows in this case to get all the visible index paths as apparently // NOTE: A prior comment claimed that `indexPathsForVisibleRows` may return extra index paths for grouped-style
// in a grouped UITableView it would return index paths for cells that are over the edge of the visible area. // tables. This is seen as an acceptable issue for the time being.
// Unfortunatly this means we never get a call for -tableView:cellForRowAtIndexPath: for that cells, but we will mark
// mark them as visible in the range controller NSIndexPath *pendingVisibleIndexPath = _pendingVisibleIndexPath;
NSMutableArray *visibleIndexPaths = [NSMutableArray array]; if (pendingVisibleIndexPath == nil) {
for (id cell in self.visibleCells) { return self.indexPathsForVisibleRows;
[visibleIndexPaths addObject:[self indexPathForCell:cell]];
} }
if (_pendingVisibleIndexPath) { NSMutableArray *visibleIndexPaths = [self.indexPathsForVisibleRows mutableCopy];
NSMutableSet *indexPaths = [NSMutableSet setWithArray:visibleIndexPaths]; [visibleIndexPaths sortUsingSelector:@selector(compare:)];
BOOL (^isAfter)(NSIndexPath *, NSIndexPath *) = ^BOOL(NSIndexPath *indexPath, NSIndexPath *anchor) { BOOL isPendingIndexPathVisible = (NSNotFound != [visibleIndexPaths indexOfObject:pendingVisibleIndexPath inSortedRange:NSMakeRange(0, visibleIndexPaths.count) options:kNilOptions usingComparator:^(id _Nonnull obj1, id _Nonnull obj2) {
if (!anchor || !indexPath) { return [obj1 compare:obj2];
return NO; }]);
}
if (indexPath.section == anchor.section) {
return (indexPath.row == anchor.row+1); // assumes that indexes are valid
} else if (indexPath.section > anchor.section && indexPath.row == 0) { if (isPendingIndexPathVisible) {
if (anchor.row != [_dataController numberOfRowsInSection:anchor.section] -1) {
return NO; // anchor is not at the end of the section
}
NSInteger nextSection = anchor.section+1;
while([_dataController numberOfRowsInSection:nextSection] == 0) {
++nextSection;
}
return indexPath.section == nextSection;
}
return NO;
};
BOOL (^isBefore)(NSIndexPath *, NSIndexPath *) = ^BOOL(NSIndexPath *indexPath, NSIndexPath *anchor) {
return isAfter(anchor, indexPath);
};
if ([indexPaths containsObject:_pendingVisibleIndexPath]) {
_pendingVisibleIndexPath = nil; // once it has shown up in visibleIndexPaths, we can stop tracking it _pendingVisibleIndexPath = nil; // once it has shown up in visibleIndexPaths, we can stop tracking it
} else if (!isBefore(_pendingVisibleIndexPath, visibleIndexPaths.firstObject) && } else if ([self isIndexPath:visibleIndexPaths.firstObject immediateSuccessorOfIndexPath:pendingVisibleIndexPath]) {
!isAfter(_pendingVisibleIndexPath, visibleIndexPaths.lastObject)) { [visibleIndexPaths insertObject:pendingVisibleIndexPath atIndex:0];
_pendingVisibleIndexPath = nil; // not contiguous, ignore. } else if ([self isIndexPath:pendingVisibleIndexPath immediateSuccessorOfIndexPath:visibleIndexPaths.lastObject]) {
[visibleIndexPaths addObject:pendingVisibleIndexPath];
} else { } else {
[indexPaths addObject:_pendingVisibleIndexPath]; _pendingVisibleIndexPath = nil; // not contiguous, ignore.
[visibleIndexPaths removeAllObjects];
[visibleIndexPaths addObjectsFromArray:[indexPaths.allObjects sortedArrayUsingSelector:@selector(compare:)]];
} }
}
return visibleIndexPaths; return visibleIndexPaths;
} }
- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController
{
return self.scrollDirection;
}
- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths - (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths
{ {
return [_dataController nodesAtIndexPaths:indexPaths]; return [_dataController nodesAtIndexPaths:indexPaths];
@@ -953,6 +927,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
_performingBatchUpdates = YES;
[super beginUpdates]; [super beginUpdates];
if (_automaticallyAdjustsContentOffset) { if (_automaticallyAdjustsContentOffset) {
@@ -978,8 +953,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
ASPerformBlockWithoutAnimation(!animated, ^{ ASPerformBlockWithoutAnimation(!animated, ^{
[super endUpdates]; [super endUpdates];
[_rangeController updateIfNeeded];
}); });
_performingBatchUpdates = NO;
if (completion) { if (completion) {
completion(YES); completion(YES);
} }
@@ -1005,6 +982,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
NSLog(@"-[super insertRowsAtIndexPaths]: %@", indexPaths); NSLog(@"-[super insertRowsAtIndexPaths]: %@", indexPaths);
} }
[super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions];
if (!_performingBatchUpdates) {
[_rangeController updateIfNeeded];
}
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count];
}); });
@@ -1028,6 +1008,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
NSLog(@"-[super deleteRowsAtIndexPaths]: %@", indexPaths); NSLog(@"-[super deleteRowsAtIndexPaths]: %@", indexPaths);
} }
[super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions];
if (!_performingBatchUpdates) {
[_rangeController updateIfNeeded];
}
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count];
}); });
@@ -1052,6 +1035,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
NSLog(@"-[super insertSections]: %@", indexSet); NSLog(@"-[super insertSections]: %@", indexSet);
} }
[super insertSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; [super insertSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions];
if (!_performingBatchUpdates) {
[_rangeController updateIfNeeded];
}
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count];
}); });
} }
@@ -1071,6 +1057,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
NSLog(@"-[super deleteSections]: %@", indexSet); NSLog(@"-[super deleteSections]: %@", indexSet);
} }
[super deleteSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; [super deleteSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions];
if (!_performingBatchUpdates) {
[_rangeController updateIfNeeded];
}
[self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count];
}); });
} }
@@ -1232,6 +1221,32 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
[_rangeController clearFetchedData]; [_rangeController clearFetchedData];
} }
#pragma mark - Helper Methods
- (BOOL)isIndexPath:(NSIndexPath *)indexPath immediateSuccessorOfIndexPath:(NSIndexPath *)anchor
{
if (!anchor || !indexPath) {
return NO;
}
if (indexPath.section == anchor.section) {
return (indexPath.row == anchor.row+1); // assumes that indexes are valid
} else if (indexPath.section > anchor.section && indexPath.row == 0) {
if (anchor.row != [_dataController numberOfRowsInSection:anchor.section] -1) {
return NO; // anchor is not at the end of the section
}
NSInteger nextSection = anchor.section+1;
while([_dataController numberOfRowsInSection:nextSection] == 0) {
++nextSection;
}
return indexPath.section == nextSection;
}
return NO;
}
#pragma mark - _ASDisplayView behavior substitutions #pragma mark - _ASDisplayView behavior substitutions
// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element. // Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element.
// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView. // Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView.
@@ -1255,7 +1270,8 @@ 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 setNeedsUpdate];
[_rangeController updateIfNeeded];
} }
} }

View File

@@ -40,12 +40,18 @@ NS_ASSUME_NONNULL_BEGIN
/** /**
* Notify the range controller that the visible range has been updated. * Notify the range controller that the visible range has been updated.
* This is the primary input call that drives updating the working ranges, and triggering their actions. * This is the primary input call that drives updating the working ranges, and triggering their actions.
* * The ranges will be updated in the next turn of the main loop, or when -updateIfNeeded is called.
* @param scrollDirection The current scroll direction of the scroll view.
* *
* @see [ASRangeControllerDelegate rangeControllerVisibleNodeIndexPaths:] * @see [ASRangeControllerDelegate rangeControllerVisibleNodeIndexPaths:]
*/ */
- (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection; - (void)setNeedsUpdate;
/**
* Update the ranges immediately, if -setNeedsUpdate has been called since the last update.
* This is useful because the ranges must be updated immediately after a cell is added
* into a table/collection to satisfy interface state API guarantees.
*/
- (void)updateIfNeeded;
/** /**
* Add the sized node for `indexPath` as a subview of `contentView`. * Add the sized node for `indexPath` as a subview of `contentView`.
@@ -101,6 +107,13 @@ NS_ASSUME_NONNULL_BEGIN
*/ */
- (NSArray<NSIndexPath *> *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController; - (NSArray<NSIndexPath *> *)visibleNodeIndexPathsForRangeController:(ASRangeController *)rangeController;
/**
* @param rangeController Sender.
*
* @returns the current scroll direction of the view using this range controller.
*/
- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController;
/** /**
* @param rangeController Sender. * @param rangeController Sender.
* *

View File

@@ -19,17 +19,23 @@
#import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+FrameworkPrivate.h"
#import "ASCellNode.h" #import "ASCellNode.h"
#define AS_RANGECONTROLLER_LOG_UPDATE_FREQ 0
@interface ASRangeController () @interface ASRangeController ()
{ {
BOOL _rangeIsValid; BOOL _rangeIsValid;
BOOL _queuedRangeUpdate; BOOL _needsRangeUpdate;
BOOL _layoutControllerImplementsSetVisibleIndexPaths; BOOL _layoutControllerImplementsSetVisibleIndexPaths;
ASScrollDirection _scrollDirection;
NSSet<NSIndexPath *> *_allPreviousIndexPaths; NSSet<NSIndexPath *> *_allPreviousIndexPaths;
ASLayoutRangeMode _currentRangeMode; ASLayoutRangeMode _currentRangeMode;
BOOL _didUpdateCurrentRange; BOOL _didUpdateCurrentRange;
BOOL _didRegisterForNodeDisplayNotifications; BOOL _didRegisterForNodeDisplayNotifications;
CFAbsoluteTime _pendingDisplayNodesTimestamp; CFAbsoluteTime _pendingDisplayNodesTimestamp;
#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ
NSUInteger _updateCountThisFrame;
CADisplayLink *_displayLink;
#endif
} }
@end @end
@@ -52,11 +58,20 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
[[[self class] allRangeControllersWeakSet] addObject:self]; [[[self class] allRangeControllersWeakSet] addObject:self];
#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_updateCountDisplayLinkDidFire)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
#endif
return self; return self;
} }
- (void)dealloc - (void)dealloc
{ {
#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ
[_displayLink invalidate];
#endif
if (_didRegisterForNodeDisplayNotifications) { if (_didRegisterForNodeDisplayNotifications) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil];
} }
@@ -94,12 +109,25 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
return selfInterfaceState; return selfInterfaceState;
} }
- (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection - (void)setNeedsUpdate
{ {
_scrollDirection = scrollDirection; if (!_needsRangeUpdate) {
_needsRangeUpdate = YES;
// Perform update immediately, so that cells receive a visibleStateDidChange: call before their first pixel is visible. __weak __typeof__(self) weakSelf = self;
[self scheduleRangeUpdate]; dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf updateIfNeeded];
});
}
}
- (void)updateIfNeeded
{
if (_needsRangeUpdate) {
_needsRangeUpdate = NO;
[self _updateVisibleNodeIndexPaths];
}
} }
- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode
@@ -108,69 +136,51 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
_currentRangeMode = rangeMode; _currentRangeMode = rangeMode;
_didUpdateCurrentRange = YES; _didUpdateCurrentRange = YES;
[self scheduleRangeUpdate]; [self setNeedsUpdate];
} }
} }
- (void)scheduleRangeUpdate
{
if (_queuedRangeUpdate) {
return;
}
// coalesce these events -- handling them multiple times per runloop is noisy and expensive
_queuedRangeUpdate = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[self performRangeUpdate];
});
}
- (void)performRangeUpdate
{
// Call this version if you want the update to occur immediately, such as on app suspend, as another runloop may not occur.
ASDisplayNodeAssertMainThread();
_queuedRangeUpdate = YES; // For now, set this flag as _update... expects it and clears it.
[self _updateVisibleNodeIndexPaths];
}
- (void)setLayoutController:(id<ASLayoutController>)layoutController - (void)setLayoutController:(id<ASLayoutController>)layoutController
{ {
_layoutController = layoutController; _layoutController = layoutController;
_layoutControllerImplementsSetVisibleIndexPaths = [_layoutController respondsToSelector:@selector(setVisibleNodeIndexPaths:)]; _layoutControllerImplementsSetVisibleIndexPaths = [_layoutController respondsToSelector:@selector(setVisibleNodeIndexPaths:)];
if (_layoutController && _queuedRangeUpdate) { if (layoutController && _dataSource) {
[self performRangeUpdate]; [self updateIfNeeded];
} }
} }
- (void)setDataSource:(id<ASRangeControllerDataSource>)dataSource - (void)setDataSource:(id<ASRangeControllerDataSource>)dataSource
{ {
_dataSource = dataSource; _dataSource = dataSource;
if (_dataSource && _queuedRangeUpdate) { if (dataSource && _layoutController) {
[self performRangeUpdate]; [self updateIfNeeded];
} }
} }
- (void)_updateVisibleNodeIndexPaths - (void)_updateVisibleNodeIndexPaths
{ {
ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController");
if (!_queuedRangeUpdate || !_layoutController || !_dataSource) { if (!_layoutController || !_dataSource) {
return; return;
} }
#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ
_updateCountThisFrame += 1;
#endif
// allNodes is a 2D array: it contains arrays for each section, each containing nodes. // allNodes is a 2D array: it contains arrays for each section, each containing nodes.
NSArray<NSArray *> *allNodes = [_dataSource completedNodes]; NSArray<NSArray *> *allNodes = [_dataSource completedNodes];
NSUInteger numberOfSections = [allNodes count]; NSUInteger numberOfSections = [allNodes count];
// TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges
// Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; // Example: ... = [_layoutController indexPathsForScrolling:scrollDirection rangeType:ASLayoutRangeTypeVisible];
NSArray<NSIndexPath *> *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; NSArray<NSIndexPath *> *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self];
if (visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... 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 == NO to make sure we update it later return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later
} }
ASScrollDirection scrollDirection = [_dataSource scrollDirectionForRangeController:self];
[_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]]; [_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]];
// the layout controller needs to know what the current visible indices are to calculate range offsets // the layout controller needs to know what the current visible indices are to calculate range offsets
@@ -203,7 +213,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
if (ASRangeTuningParametersEqualToRangeTuningParameters(parametersFetchData, ASRangeTuningParametersZero)) { if (ASRangeTuningParametersEqualToRangeTuningParameters(parametersFetchData, ASRangeTuningParametersZero)) {
fetchDataIndexPaths = visibleIndexPaths; fetchDataIndexPaths = visibleIndexPaths;
} else { } else {
fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection fetchDataIndexPaths = [_layoutController indexPathsForScrolling:scrollDirection
rangeMode:rangeMode rangeMode:rangeMode
rangeType:ASLayoutRangeTypeFetchData]; rangeType:ASLayoutRangeTypeFetchData];
} }
@@ -217,7 +227,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
} else if (ASRangeTuningParametersEqualToRangeTuningParameters(parametersDisplay, parametersFetchData)) { } else if (ASRangeTuningParametersEqualToRangeTuningParameters(parametersDisplay, parametersFetchData)) {
displayIndexPaths = fetchDataIndexPaths; displayIndexPaths = fetchDataIndexPaths;
} else { } else {
displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection displayIndexPaths = [_layoutController indexPathsForScrolling:scrollDirection
rangeMode:rangeMode rangeMode:rangeMode
rangeType:ASLayoutRangeTypeDisplay]; rangeType:ASLayoutRangeTypeDisplay];
} }
@@ -322,7 +332,6 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
} }
_rangeIsValid = YES; _rangeIsValid = YES;
_queuedRangeUpdate = NO;
#if ASRangeControllerLoggingEnabled #if ASRangeControllerLoggingEnabled
// NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; // NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
@@ -363,7 +372,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive;
[[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil];
_didRegisterForNodeDisplayNotifications = NO; _didRegisterForNodeDisplayNotifications = NO;
[self scheduleRangeUpdate]; [self setNeedsUpdate];
} }
} }
@@ -509,7 +518,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible
for (ASRangeController *rangeController in allRangeControllers) { for (ASRangeController *rangeController in allRangeControllers) {
BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]);
[rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings]; [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings];
[rangeController performRangeUpdate]; [rangeController setNeedsUpdate];
[rangeController updateIfNeeded];
} }
#if ASRangeControllerLoggingEnabled #if ASRangeControllerLoggingEnabled
@@ -531,7 +541,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible
__ApplicationState = UIApplicationStateBackground; __ApplicationState = UIApplicationStateBackground;
for (ASRangeController *rangeController in allRangeControllers) { for (ASRangeController *rangeController in allRangeControllers) {
// Trigger a range update immediately, as we may not be allowed by the system to run the update block scheduled by changing range mode. // Trigger a range update immediately, as we may not be allowed by the system to run the update block scheduled by changing range mode.
[rangeController performRangeUpdate]; [rangeController setNeedsUpdate];
[rangeController updateIfNeeded];
} }
#if ASRangeControllerLoggingEnabled #if ASRangeControllerLoggingEnabled
@@ -546,7 +557,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible
for (ASRangeController *rangeController in allRangeControllers) { for (ASRangeController *rangeController in allRangeControllers) {
BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]);
[rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly];
[rangeController performRangeUpdate]; [rangeController setNeedsUpdate];
[rangeController updateIfNeeded];
} }
#if ASRangeControllerLoggingEnabled #if ASRangeControllerLoggingEnabled
@@ -556,6 +568,16 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible
#pragma mark - Debugging #pragma mark - Debugging
#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ
- (void)_updateCountDisplayLinkDidFire
{
if (_updateCountThisFrame > 1) {
NSLog(@"ASRangeController %p updated %lu times this frame.", self, (unsigned long)_updateCountThisFrame);
}
_updateCountThisFrame = 0;
}
#endif
- (NSString *)descriptionWithIndexPaths:(NSArray<NSIndexPath *> *)indexPaths - (NSString *)descriptionWithIndexPaths:(NSArray<NSIndexPath *> *)indexPaths
{ {
NSMutableString *description = [NSMutableString stringWithFormat:@"%@ %@", [super description], @" allPreviousIndexPaths:\n"]; NSMutableString *description = [NSMutableString stringWithFormat:@"%@ %@", [super description], @" allPreviousIndexPaths:\n"];

View File

@@ -13,7 +13,8 @@
@protocol ASRangeControllerUpdateRangeProtocol <NSObject> @protocol ASRangeControllerUpdateRangeProtocol <NSObject>
/** /**
* Updates the current range mode of the range controller for at least the next range update. * Updates the current range mode of the range controller for at least the next range update
* and, if the new mode is different from the previous mode, enqueues a range update.
*/ */
- (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode; - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode;