diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index 9290fa1432..1f618805ae 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -272,6 +272,10 @@ { [super visibleStateDidChange:isVisible]; + if (isVisible && self.neverShowPlaceholders) { + [self recursivelyEnsureDisplaySynchronously:YES]; + } + // 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."); UIView *view = self.view; diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index d835a88a1f..857245573f 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -639,11 +639,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; [_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:))) { [_cellsForVisibilityUpdates addObject:cell]; } @@ -651,8 +648,6 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(_ASCollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; - ASCellNode *cellNode = [cell node]; if (_asyncDelegateFlags.asyncDelegateCollectionViewDidEndDisplayingNodeForItemAtIndexPath) { @@ -660,9 +655,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; } - if ([_cellsForVisibilityUpdates containsObject:cell]) { - [_cellsForVisibilityUpdates removeObject:cell]; - } + [_rangeController setNeedsUpdate]; + + [_cellsForVisibilityUpdates removeObject:cell]; #pragma clang diagnostic push #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 [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 { ASDisplayNodeAssertMainThread(); - - // Calling visibleNodeIndexPathsForRangeController: will trigger UIKit to call reloadData if it never has, which can result + // Calling -indexPathsForVisibleItems 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. BOOL isZeroSized = CGRectEqualToRect(self.bounds, CGRectZero); return isZeroSized ? @[] : [self indexPathsForVisibleItems]; } +- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController +{ + return self.scrollDirection; +} + - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController { ASDisplayNodeAssertMainThread(); @@ -1085,9 +1091,13 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; block(); } } completion:^(BOOL finished){ + // Flush any range changes that happened as part of the update animations ending. + [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:numberOfUpdateBlocks]; if (completion) { completion(finished); } }]; + // Flush any range changes that happened as part of submitting the update. + [_rangeController updateIfNeeded]; }); [_batchUpdateBlocks removeAllObjects]; @@ -1114,6 +1124,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } else { [UIView performWithoutAnimation:^{ [super insertItemsAtIndexPaths:indexPaths]; + // Flush any range changes that happened as part of submitting the update. + [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; }]; } @@ -1134,6 +1146,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } else { [UIView performWithoutAnimation:^{ [super deleteItemsAtIndexPaths:indexPaths]; + // Flush any range changes that happened as part of submitting the update. + [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; }]; } @@ -1154,6 +1168,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } else { [UIView performWithoutAnimation:^{ [super insertSections:indexSet]; + // Flush any range changes that happened as part of submitting the update. + [_rangeController updateIfNeeded]; [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; }]; } @@ -1174,6 +1190,8 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } else { [UIView performWithoutAnimation:^{ [super deleteSections:indexSet]; + // Flush any range changes that happened as part of submitting the update. + [_rangeController updateIfNeeded]; [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 // their update in the layout pass if (![node supportsRangeManagedInterfaceState]) { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + [_rangeController setNeedsUpdate]; + [_rangeController updateIfNeeded]; } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 5e9f128b38..80bf807308 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -23,6 +23,7 @@ #import "ASLayout.h" #import "_ASDisplayLayer.h" #import "ASTableNode.h" +#import "ASEqualityHelpers.h" static const ASSizeRange kInvalidSizeRange = {CGSizeZero, CGSizeZero}; static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; @@ -126,6 +127,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; BOOL _ignoreNodesConstrainedWidthChange; BOOL _queuedNodeHeightUpdate; BOOL _isDeallocating; + BOOL _performingBatchUpdates; NSMutableSet *_cellsForVisibilityUpdates; 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 [super layoutSubviews]; + [_rangeController updateIfNeeded]; } #pragma mark - @@ -644,35 +647,29 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_asyncDelegate tableView:self willDisplayNodeForRowAtIndexPath:indexPath]; } - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; - - if (cellNode.neverShowPlaceholders) { - [cellNode recursivelyEnsureDisplaySynchronously:YES]; - } + [_rangeController setNeedsUpdate]; if (ASSubclassOverridesSelector([ASCellNode class], [cellNode class], @selector(cellNodeVisibilityEvent:inScrollView:withCellFrame:))) { [_cellsForVisibilityUpdates addObject:cell]; } } -- (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; } ASCellNode *cellNode = [cell node]; - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; + [_rangeController setNeedsUpdate]; if (_asyncDelegateFlags.asyncDelegateTableViewDidEndDisplayingNodeForRowAtIndexPath) { ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with removed cell not to be nil."); [_asyncDelegate tableView:self didEndDisplayingNode:cellNode forRowAtIndexPath:indexPath]; } - if ([_cellsForVisibilityUpdates containsObject:cell]) { - [_cellsForVisibilityUpdates removeObject:cell]; - } + [_cellsForVisibilityUpdates removeObject:cell]; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" @@ -866,61 +863,38 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return @[]; } - // In this case we cannot use indexPathsForVisibleRows in this case to get all the visible index paths as apparently - // in a grouped UITableView it would return index paths for cells that are over the edge of the visible area. - // 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 - NSMutableArray *visibleIndexPaths = [NSMutableArray array]; - for (id cell in self.visibleCells) { - [visibleIndexPaths addObject:[self indexPathForCell:cell]]; + // NOTE: A prior comment claimed that `indexPathsForVisibleRows` may return extra index paths for grouped-style + // tables. This is seen as an acceptable issue for the time being. + + NSIndexPath *pendingVisibleIndexPath = _pendingVisibleIndexPath; + if (pendingVisibleIndexPath == nil) { + return self.indexPathsForVisibleRows; } - if (_pendingVisibleIndexPath) { - NSMutableSet *indexPaths = [NSMutableSet setWithArray:visibleIndexPaths]; - - BOOL (^isAfter)(NSIndexPath *, NSIndexPath *) = ^BOOL(NSIndexPath *indexPath, 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; - }; - - 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 - } else if (!isBefore(_pendingVisibleIndexPath, visibleIndexPaths.firstObject) && - !isAfter(_pendingVisibleIndexPath, visibleIndexPaths.lastObject)) { - _pendingVisibleIndexPath = nil; // not contiguous, ignore. - } else { - [indexPaths addObject:_pendingVisibleIndexPath]; - - [visibleIndexPaths removeAllObjects]; - [visibleIndexPaths addObjectsFromArray:[indexPaths.allObjects sortedArrayUsingSelector:@selector(compare:)]]; - } - } + NSMutableArray *visibleIndexPaths = [self.indexPathsForVisibleRows mutableCopy]; + [visibleIndexPaths sortUsingSelector:@selector(compare:)]; + + BOOL isPendingIndexPathVisible = (NSNotFound != [visibleIndexPaths indexOfObject:pendingVisibleIndexPath inSortedRange:NSMakeRange(0, visibleIndexPaths.count) options:kNilOptions usingComparator:^(id _Nonnull obj1, id _Nonnull obj2) { + return [obj1 compare:obj2]; + }]); + if (isPendingIndexPathVisible) { + _pendingVisibleIndexPath = nil; // once it has shown up in visibleIndexPaths, we can stop tracking it + } else if ([self isIndexPath:visibleIndexPaths.firstObject immediateSuccessorOfIndexPath:pendingVisibleIndexPath]) { + [visibleIndexPaths insertObject:pendingVisibleIndexPath atIndex:0]; + } else if ([self isIndexPath:pendingVisibleIndexPath immediateSuccessorOfIndexPath:visibleIndexPaths.lastObject]) { + [visibleIndexPaths addObject:pendingVisibleIndexPath]; + } else { + _pendingVisibleIndexPath = nil; // not contiguous, ignore. + } return visibleIndexPaths; } +- (ASScrollDirection)scrollDirectionForRangeController:(ASRangeController *)rangeController +{ + return self.scrollDirection; +} + - (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)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 } + _performingBatchUpdates = YES; [super beginUpdates]; if (_automaticallyAdjustsContentOffset) { @@ -978,8 +953,10 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; ASPerformBlockWithoutAnimation(!animated, ^{ [super endUpdates]; + [_rangeController updateIfNeeded]; }); + _performingBatchUpdates = NO; if (completion) { completion(YES); } @@ -1005,6 +982,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; NSLog(@"-[super insertRowsAtIndexPaths]: %@", indexPaths); } [super insertRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; + if (!_performingBatchUpdates) { + [_rangeController updateIfNeeded]; + } [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; }); @@ -1028,6 +1008,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; NSLog(@"-[super deleteRowsAtIndexPaths]: %@", indexPaths); } [super deleteRowsAtIndexPaths:indexPaths withRowAnimation:(UITableViewRowAnimation)animationOptions]; + if (!_performingBatchUpdates) { + [_rangeController updateIfNeeded]; + } [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexPaths.count]; }); @@ -1052,6 +1035,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; NSLog(@"-[super insertSections]: %@", indexSet); } [super insertSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; + if (!_performingBatchUpdates) { + [_rangeController updateIfNeeded]; + } [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; }); } @@ -1071,6 +1057,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; NSLog(@"-[super deleteSections]: %@", indexSet); } [super deleteSections:indexSet withRowAnimation:(UITableViewRowAnimation)animationOptions]; + if (!_performingBatchUpdates) { + [_rangeController updateIfNeeded]; + } [self _scheduleCheckForBatchFetchingForNumberOfChanges:indexSet.count]; }); } @@ -1232,6 +1221,32 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; [_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 // 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. @@ -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 // their update in the layout pass if (![node supportsRangeManagedInterfaceState]) { - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:[self scrollDirection]]; + [_rangeController setNeedsUpdate]; + [_rangeController updateIfNeeded]; } } diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 6341e1dd17..e4a883395d 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -40,12 +40,18 @@ NS_ASSUME_NONNULL_BEGIN /** * 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. - * - * @param scrollDirection The current scroll direction of the scroll view. + * The ranges will be updated in the next turn of the main loop, or when -updateIfNeeded is called. * * @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`. @@ -101,6 +107,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSArray *)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. * @@ -222,4 +235,4 @@ NS_ASSUME_NONNULL_BEGIN @end -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 5dfa056503..dc051decc8 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -19,17 +19,23 @@ #import "ASDisplayNode+FrameworkPrivate.h" #import "ASCellNode.h" +#define AS_RANGECONTROLLER_LOG_UPDATE_FREQ 0 + @interface ASRangeController () { BOOL _rangeIsValid; - BOOL _queuedRangeUpdate; + BOOL _needsRangeUpdate; BOOL _layoutControllerImplementsSetVisibleIndexPaths; - ASScrollDirection _scrollDirection; NSSet *_allPreviousIndexPaths; ASLayoutRangeMode _currentRangeMode; BOOL _didUpdateCurrentRange; BOOL _didRegisterForNodeDisplayNotifications; CFAbsoluteTime _pendingDisplayNodesTimestamp; + +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + NSUInteger _updateCountThisFrame; + CADisplayLink *_displayLink; +#endif } @end @@ -52,11 +58,20 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [[[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; } - (void)dealloc { +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + [_displayLink invalidate]; +#endif + if (_didRegisterForNodeDisplayNotifications) { [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; } @@ -94,12 +109,25 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; return selfInterfaceState; } -- (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection +- (void)setNeedsUpdate { - _scrollDirection = scrollDirection; + if (!_needsRangeUpdate) { + _needsRangeUpdate = YES; + + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateIfNeeded]; + }); + } +} - // Perform update immediately, so that cells receive a visibleStateDidChange: call before their first pixel is visible. - [self scheduleRangeUpdate]; +- (void)updateIfNeeded +{ + if (_needsRangeUpdate) { + _needsRangeUpdate = NO; + + [self _updateVisibleNodeIndexPaths]; + } } - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode @@ -107,70 +135,52 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; if (_currentRangeMode != rangeMode) { _currentRangeMode = rangeMode; _didUpdateCurrentRange = YES; - - [self scheduleRangeUpdate]; - } -} -- (void)scheduleRangeUpdate -{ - if (_queuedRangeUpdate) { - return; + [self setNeedsUpdate]; } - - // 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)layoutController { _layoutController = layoutController; _layoutControllerImplementsSetVisibleIndexPaths = [_layoutController respondsToSelector:@selector(setVisibleNodeIndexPaths:)]; - if (_layoutController && _queuedRangeUpdate) { - [self performRangeUpdate]; + if (layoutController && _dataSource) { + [self updateIfNeeded]; } } - (void)setDataSource:(id)dataSource { _dataSource = dataSource; - if (_dataSource && _queuedRangeUpdate) { - [self performRangeUpdate]; + if (dataSource && _layoutController) { + [self updateIfNeeded]; } } - (void)_updateVisibleNodeIndexPaths { ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); - if (!_queuedRangeUpdate || !_layoutController || !_dataSource) { + if (!_layoutController || !_dataSource) { return; } +#if AS_RANGECONTROLLER_LOG_UPDATE_FREQ + _updateCountThisFrame += 1; +#endif + // allNodes is a 2D array: it contains arrays for each section, each containing nodes. NSArray *allNodes = [_dataSource completedNodes]; 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 - // Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; + // Example: ... = [_layoutController indexPathsForScrolling:scrollDirection rangeType:ASLayoutRangeTypeVisible]; NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController: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 == NO to make sure we update it later } + ASScrollDirection scrollDirection = [_dataSource scrollDirectionForRangeController:self]; [_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]]; // 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)) { fetchDataIndexPaths = visibleIndexPaths; } else { - fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection + fetchDataIndexPaths = [_layoutController indexPathsForScrolling:scrollDirection rangeMode:rangeMode rangeType:ASLayoutRangeTypeFetchData]; } @@ -217,7 +227,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } else if (ASRangeTuningParametersEqualToRangeTuningParameters(parametersDisplay, parametersFetchData)) { displayIndexPaths = fetchDataIndexPaths; } else { - displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection + displayIndexPaths = [_layoutController indexPathsForScrolling:scrollDirection rangeMode:rangeMode rangeType:ASLayoutRangeTypeDisplay]; } @@ -322,7 +332,6 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } _rangeIsValid = YES; - _queuedRangeUpdate = NO; #if ASRangeControllerLoggingEnabled // NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; @@ -363,7 +372,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [[NSNotificationCenter defaultCenter] removeObserver:self name:ASRenderingEngineDidDisplayScheduledNodesNotification object:nil]; _didRegisterForNodeDisplayNotifications = NO; - [self scheduleRangeUpdate]; + [self setNeedsUpdate]; } } @@ -509,7 +518,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible for (ASRangeController *rangeController in allRangeControllers) { BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings]; - [rangeController performRangeUpdate]; + [rangeController setNeedsUpdate]; + [rangeController updateIfNeeded]; } #if ASRangeControllerLoggingEnabled @@ -531,7 +541,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible __ApplicationState = UIApplicationStateBackground; 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. - [rangeController performRangeUpdate]; + [rangeController setNeedsUpdate]; + [rangeController updateIfNeeded]; } #if ASRangeControllerLoggingEnabled @@ -546,7 +557,8 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible for (ASRangeController *rangeController in allRangeControllers) { BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; - [rangeController performRangeUpdate]; + [rangeController setNeedsUpdate]; + [rangeController updateIfNeeded]; } #if ASRangeControllerLoggingEnabled @@ -556,6 +568,16 @@ static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisible #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 *)indexPaths { NSMutableString *description = [NSMutableString stringWithFormat:@"%@ %@", [super description], @" allPreviousIndexPaths:\n"]; diff --git a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h index 831246f33f..86230f84f8 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h +++ b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h @@ -13,7 +13,8 @@ @protocol ASRangeControllerUpdateRangeProtocol /** - * 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;