From 984fe4399736fa4d4807ec87e8c7a522085454e1 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Sun, 3 Jan 2016 19:14:07 -0800 Subject: [PATCH] [ASRangeController] Inspect delegate's ASInterfaceState to delay preloading beyond viewport until visible. --- AsyncDisplayKit/ASCollectionNode.mm | 12 +- AsyncDisplayKit/ASCollectionView.mm | 26 ++++ AsyncDisplayKit/ASDisplayNode.h | 2 - AsyncDisplayKit/ASTableNode.m | 16 ++- AsyncDisplayKit/ASTableView.mm | 29 +++++ AsyncDisplayKit/ASTableViewInternal.h | 1 + AsyncDisplayKit/Details/ASRangeController.h | 11 ++ .../Details/ASRangeControllerBeta.mm | 118 +++++++++++++----- .../Private/ASDisplayNode+FrameworkPrivate.h | 7 ++ .../Private/ASDisplayNodeInternal.h | 8 -- 10 files changed, 181 insertions(+), 49 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index 292d6d8851..f184a969a2 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -9,6 +9,7 @@ #import "ASCollectionNode.h" #import "ASCollectionInternal.h" #import "ASDisplayNode+Subclasses.h" +#import "ASRangeController.h" #include @interface _ASCollectionPendingState : NSObject @@ -97,12 +98,12 @@ { [super didLoad]; + ASCollectionView *view = self.view; + view.collectionNode = self; + if (_pendingState) { _ASCollectionPendingState *pendingState = _pendingState; - self.pendingState = nil; - - ASCollectionView *view = self.view; - view.collectionNode = self; + self.pendingState = nil; view.asyncDelegate = pendingState.delegate; view.asyncDataSource = pendingState.dataSource; } @@ -160,10 +161,13 @@ return (ASCollectionView *)[super view]; } +#if RangeControllerLoggingEnabled - (void)visibilityDidChange:(BOOL)isVisible { + [super visibilityDidChange:isVisible]; NSLog(@"%@ - visible: %d", self, isVisible); } +#endif - (void)clearContents { diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 4473e371a4..f42ec5b6b0 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -782,6 +782,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return self.bounds.size; } +- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController +{ + return self.collectionNode.interfaceState; +} + - (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths { return [_dataController nodesAtIndexPaths:indexPaths]; @@ -944,6 +949,27 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; } } +#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. +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + BOOL visible = (newWindow != nil); + ASDisplayNode *node = self.collectionNode; + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + BOOL visible = (self.window != nil); + ASDisplayNode *node = self.collectionNode; + if (!visible && node.inHierarchy) { + [node __exitHierarchy]; + } +} + #pragma mark - UICollectionView dead-end intercepts #if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting. diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 3e2a32fb44..a38f093ca2 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -66,8 +66,6 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState) ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay | ASInterfaceStateVisible, }; - - /** * An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view * hierarchy off the main thread, and could do rendering off the main thread as well. diff --git a/AsyncDisplayKit/ASTableNode.m b/AsyncDisplayKit/ASTableNode.m index e81366ed99..f81623aeb7 100644 --- a/AsyncDisplayKit/ASTableNode.m +++ b/AsyncDisplayKit/ASTableNode.m @@ -9,6 +9,7 @@ #import "ASFlowLayoutController.h" #import "ASTableViewInternal.h" #import "ASDisplayNode+Subclasses.h" +#import "ASRangeController.h" @interface _ASTablePendingState : NSObject @property (weak, nonatomic) id delegate; @@ -63,11 +64,12 @@ { [super didLoad]; + ASTableView *view = self.view; + view.tableNode = self; + if (_pendingState) { _ASTablePendingState *pendingState = _pendingState; - self.pendingState = nil; - - ASTableView *view = self.view; + self.pendingState = nil; view.asyncDelegate = pendingState.delegate; view.asyncDataSource = pendingState.dataSource; } @@ -125,6 +127,14 @@ return (ASTableView *)[super view]; } +#if RangeControllerLoggingEnabled +- (void)visibilityDidChange:(BOOL)isVisible +{ + [super visibilityDidChange:isVisible]; + NSLog(@"%@ - visible: %d", self, isVisible); +} +#endif + - (void)clearContents { [super clearContents]; diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 679a00d46a..7632a7c63d 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -121,6 +121,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; // This also permits sharing logic with ASCollectionNode, as the superclass is not UIKit-controlled. @property (nonatomic, retain) ASTableNode *strongTableNode; +// Always set, whether ASCollectionView is created directly or via ASCollectionNode. +@property (nonatomic, weak) ASTableNode *tableNode; + @end @implementation ASTableView @@ -700,6 +703,11 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return self.bounds.size; } +- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController +{ + return self.tableNode.interfaceState; +} + #pragma mark - ASRangeControllerDelegate - (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController @@ -943,4 +951,25 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; } } +#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. +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + BOOL visible = (newWindow != nil); + ASDisplayNode *node = self.tableNode; + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + BOOL visible = (self.window != nil); + ASDisplayNode *node = self.tableNode; + if (!visible && node.inHierarchy) { + [node __exitHierarchy]; + } +} + @end diff --git a/AsyncDisplayKit/ASTableViewInternal.h b/AsyncDisplayKit/ASTableViewInternal.h index 02eb062d46..cc93c44a84 100644 --- a/AsyncDisplayKit/ASTableViewInternal.h +++ b/AsyncDisplayKit/ASTableViewInternal.h @@ -13,6 +13,7 @@ @interface ASTableView (Internal) @property (nonatomic, retain, readonly) ASDataController *dataController; +@property (nonatomic, weak, readwrite) ASTableNode *tableNode; /** * Initializer. diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 6ffc16377d..c8dee77e11 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -12,6 +12,8 @@ #import #import +#define RangeControllerLoggingEnabled 0 + NS_ASSUME_NONNULL_BEGIN @protocol ASRangeControllerDataSource; @@ -102,6 +104,15 @@ NS_ASSUME_NONNULL_BEGIN */ - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController; +/** + * @param rangeController Sender. + * + * @returns the ASInterfaceState of the node that this controller is powering. This allows nested range controllers + * to collaborate with one another, as an outer controller may set bits in .interfaceState such as Visible. + * If this controller is an orthogonally scrolling element, it waits until it is visible to preload outside the viewport. + */ +- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController; + - (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths; - (ASDisplayNode *)rangeController:(ASRangeController *)rangeController nodeAtIndexPath:(NSIndexPath *)indexPath; diff --git a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm index ff823b01f0..979fdf320d 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerBeta.mm +++ b/AsyncDisplayKit/Details/ASRangeControllerBeta.mm @@ -17,6 +17,21 @@ #import "ASInternalHelpers.h" #import "ASDisplayNode+FrameworkPrivate.h" +extern BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateVisible) == ASInterfaceStateVisible); +} + +extern BOOL ASInterfaceStateIncludesDisplay(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateDisplay) == ASInterfaceStateDisplay); +} + +extern BOOL ASInterfaceStateIncludesFetchData(ASInterfaceState interfaceState) +{ + return ((interfaceState & ASInterfaceStateFetchData) == ASInterfaceStateFetchData); +} + @interface ASRangeControllerBeta () { BOOL _rangeIsValid; @@ -79,52 +94,91 @@ [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; } - NSSet *fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData]; - NSSet *displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay]; + ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; + NSSet *visibleIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; - - //NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; - //NSLog(@"visible sets are equal: %d", [visibleIndexPaths isEqualToSet:visibleNodePathsSet]); - - // Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. - NSMutableSet *allIndexPaths = [fetchDataIndexPaths mutableCopy]; - [allIndexPaths unionSet:displayIndexPaths]; - [allIndexPaths unionSet:visibleIndexPaths]; +#if RangeControllerLoggingEnabled NSMutableArray *modified = [NSMutableArray array]; +#endif - for (NSIndexPath *indexPath in allIndexPaths) { - // Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it. - // For consistency, make sure each node knows that it should measure itself if something changes. - ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout; + if (ASInterfaceStateIncludesVisible(selfInterfaceState)) { + // If we are already visible, get busy! Better get started on preloading before the user scrolls more... + NSSet *fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData]; + NSSet *displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay]; + + // Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint. + NSMutableSet *allIndexPaths = [fetchDataIndexPaths mutableCopy]; + [allIndexPaths unionSet:displayIndexPaths]; + [allIndexPaths unionSet:visibleIndexPaths]; - if ([fetchDataIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateFetchData; - } - if ([displayIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateDisplay; - } - if ([visibleIndexPaths containsObject:indexPath]) { - interfaceState |= ASInterfaceStateVisible; + for (NSIndexPath *indexPath in allIndexPaths) { + // Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it. + // For consistency, make sure each node knows that it should measure itself if something changes. + ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout; + + if ([fetchDataIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateFetchData; + } + if ([displayIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateDisplay; + } + if ([visibleIndexPaths containsObject:indexPath]) { + interfaceState |= ASInterfaceStateVisible; + } + + ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath]; + ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset."); + // Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. + if (node.interfaceState != interfaceState) { +#if RangeControllerLoggingEnabled + [modified addObject:indexPath]; +#endif + [node recursivelySetInterfaceState:interfaceState]; + } } + } else { + // If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the + // instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet. + // We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:. - ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath]; - ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset."); - // Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. - if (node.interfaceState != interfaceState) { - [modified addObject:indexPath]; - [node recursivelySetInterfaceState:interfaceState]; + for (NSIndexPath *indexPath in visibleIndexPaths) { + // Set Layout, Fetch Data, Display. DO NOT set Visible: even though these elements are in the visible range / "viewport", + // our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above. + ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay; + + ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath]; + ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset."); + // Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. + if (node.interfaceState != interfaceState) { +#if RangeControllerLoggingEnabled + [modified addObject:indexPath]; +#endif + [node recursivelySetInterfaceState:interfaceState]; + } } } -/* +#if RangeControllerLoggingEnabled + NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; + BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet]; + NSLog(@"visible sets are equal: %d", setsAreEqual); + if (!setsAreEqual) { + NSLog(@"standard: %@", visibleIndexPaths); + NSLog(@"custom: %@", visibleNodePathsSet); + } + [modified sortUsingSelector:@selector(compare:)]; for (NSIndexPath *indexPath in modified) { - NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, [visibleIndexPaths containsObject:indexPath], [displayIndexPaths containsObject:indexPath], [fetchDataIndexPaths containsObject:indexPath]); + ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath]; + ASInterfaceState interfaceState = node.interfaceState; + BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState); + BOOL inDisplay = ASInterfaceStateIncludesDisplay(interfaceState); + BOOL inFetchData = ASInterfaceStateIncludesFetchData(interfaceState); + NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, inVisible, inDisplay, inFetchData); } -*/ - +#endif _rangeIsValid = YES; _queuedRangeUpdate = NO; diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 896e82668f..d3a5e711af 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -66,6 +66,13 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState) - (void)enterHierarchyState:(ASHierarchyState)hierarchyState; - (void)exitHierarchyState:(ASHierarchyState)hierarchyState; +// Changed before calling willEnterHierarchy / didExitHierarchy. +@property (nonatomic, readwrite, assign, getter = isInHierarchy) BOOL inHierarchy; +// Call willEnterHierarchy if necessary and set inHierarchy = YES if visibility notifications are enabled on all of its parents +- (void)__enterHierarchy; +// Call didExitHierarchy if necessary and set inHierarchy = NO if visibility notifications are enabled on all of its parents +- (void)__exitHierarchy; + /** * @abstract Returns the Hierarchy State of the node. * diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 1636e5a175..c1f752002d 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -132,20 +132,12 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) - (void)__layout; - (void)__setSupernode:(ASDisplayNode *)supernode; -// Changed before calling willEnterHierarchy / didExitHierarchy. -@property (nonatomic, readwrite, assign, getter = isInHierarchy) BOOL inHierarchy; - // Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this. - (BOOL)__visibilityNotificationsDisabled; - (BOOL)__selfOrParentHasVisibilityNotificationsDisabled; - (void)__incrementVisibilityNotificationsDisabled; - (void)__decrementVisibilityNotificationsDisabled; -// Call willEnterHierarchy if necessary and set inHierarchy = YES if visibility notifications are enabled on all of its parents -- (void)__enterHierarchy; -// Call didExitHierarchy if necessary and set inHierarchy = NO if visibility notifications are enabled on all of its parents -- (void)__exitHierarchy; - // Helper method to summarize whether or not the node run through the display process - (BOOL)__implementsDisplay;