diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index f7dc176c04..b1d62a2c3d 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -918,14 +918,15 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController { ASCollectionNode *collectionNode = self.collectionNode; - if (collectionNode) { + if (collectionNode && [collectionNode supportsRangeManagedInterfaceState]) { + // Only use the interfaceStatate of nodes that are range managed return collectionNode.interfaceState; - } else { - // Until we can always create an associated ASCollectionNode without a retain cycle, - // we might be on our own to try to guess if we're visible. The node normally - // handles this even if it is the root / directly added to the view hierarchy. - return (self.window != nil ? ASInterfaceStateVisible : ASInterfaceStateNone); } + + // For not range managed nodes or until we can always create an associated ASCollectionNode + // without a retain cycle, we might be on our own to try to guess if we're visible. The node normally + // handles this even if it is the root / directly added to the view hierarchy. + return (self.window != nil ? ASInterfaceStateVisible : ASInterfaceStateNone); } - (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths @@ -1144,9 +1145,12 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; if (!visible && node.inHierarchy) { [node __exitHierarchy]; } - - // Trigger updating interfaceState for cells in case ASCollectionView becomes visible or invisible - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + // Updating the visible node index paths only for range managed nodes. Not range managed nodes will get their + // their update in the layout pass + if (![node supportsRangeManagedInterfaceState]) { + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + } } #pragma mark - UICollectionView dead-end intercepts diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index f4bd375876..9845309e1e 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1980,13 +1980,9 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) // subclass override } -/** - * We currently only set interface state on nodes in table/collection views. For other nodes, if they are - * in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`. - */ - (BOOL)supportsRangeManagedInterfaceState { - return (_hierarchyState & ASHierarchyStateRangeManaged); + return ASHierarchyStateIncludesRangeManaged(_hierarchyState); } - (ASInterfaceState)interfaceState diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 75f0020c62..e01c5a521a 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -795,14 +795,15 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController { ASTableNode *tableNode = self.tableNode; - if (tableNode) { + if (tableNode && [tableNode supportsRangeManagedInterfaceState]) { + // Only use the interfaceStatate of nodes that are range managed return self.tableNode.interfaceState; - } else { - // Until we can always create an associated ASTableNode without a retain cycle, - // we might be on our own to try to guess if we're visible. The node normally - // handles this even if it is the root / directly added to the view hierarchy. - return (self.window != nil ? ASInterfaceStateVisible : ASInterfaceStateNone); } + + // For not range managed nodes or until we can always create an associated ASTableNode + // without a retain cycle, we might be on our own to try to guess if we're visible. The node normally + // handles this even if it is the root / directly added to the view hierarchy. + return (self.window != nil ? ASInterfaceStateVisible : ASInterfaceStateNone); } #pragma mark - ASRangeControllerDelegate @@ -1076,9 +1077,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; if (!visible && node.inHierarchy) { [node __exitHierarchy]; } - - // Trigger updating interfaceState for cells in case ASTableView becomes visible or invisible - [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + + // Updating the visible node index paths only for range managed nodes. Not range managed nodes will get their + // their update in the layout pass + if (![node supportsRangeManagedInterfaceState]) { + [_rangeController visibleNodeIndexPathsDidChangeWithScrollDirection:self.scrollDirection]; + } } @end diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 47bc7a7683..761a74a606 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -189,7 +189,9 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; ASInterfaceState selfInterfaceState = [self interfaceState]; ASLayoutRangeMode rangeMode = _currentRangeMode; - if (!_didUpdateCurrentRange) { + // If the range mode is explicitly set via updateCurrentRangeWithMode: it will last in that mode until the + // range controller becomes visible again or explicitly changes the range mode again + if (!_didUpdateCurrentRange || ASInterfaceStateIncludesVisible(selfInterfaceState)) { rangeMode = [ASRangeController rangeModeForInterfaceState:selfInterfaceState currentRangeMode:_currentRangeMode]; } @@ -232,7 +234,10 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; _allPreviousIndexPaths = allCurrentIndexPaths; _currentRangeMode = rangeMode; - _didUpdateCurrentRange = NO; + // Reset the current range mode only if the range controller comes visible + if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { + _didUpdateCurrentRange = NO; + } if (!_rangeIsValid) { [allIndexPaths addObjectsFromArray:ASIndexPathsForTwoDimensionalArray(allNodes)]; diff --git a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h index 1a36e57662..99c054ed21 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h +++ b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h @@ -12,6 +12,18 @@ #import "ASCollectionNode.h" #import "ASTableNode.h" +/** + * Update the range mode for a range controller to a specific range mode until the node that contains the range + * controller becomes visible again + * + * Logic for the automatic range mode: + * 1. If there are no visible node paths available nothing is to be done and no range update is done + * 2. The initial range update always will be ASLayoutRangeModeCount (ASLayoutRangeModeMinimum) as it's the initial fetch + * 3. If the range mode is explicitly set via updateCurrentRangeWithMode: it will last in that mode until the range controller becomes visible and a new range update was triggered or a new range mode via updateCurrentRangeWithMode: is set + * 4. If range mode is not explicitly set the range mode is variying based if the range controller is visible or not + */ + + @protocol ASRangeControllerUpdateRangeProtocol diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 79f2b2e273..efcd2e5e46 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -56,6 +56,11 @@ inline BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyStat return ((hierarchyState & ASHierarchyStateLayoutPending) == ASHierarchyStateLayoutPending); } +inline BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState hierarchyState) +{ + return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged); +} + @interface ASDisplayNode () { @protected @@ -91,6 +96,14 @@ inline BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyStat */ @property (nonatomic, readwrite) ASHierarchyState hierarchyState; +/** + * @abstract Return if the node is range managed or not + * + * @discussion Currently only set interface state on nodes in table and collection views. For other nodes, if they are + * in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`. + */ +- (BOOL)supportsRangeManagedInterfaceState; + // The two methods below will eventually be exposed, but their names are subject to change. /** * @abstract Ensure that all rendering is complete for this node and its descendents.