Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 005408b

Browse files
committed
Merge pull request #1030 from facebook/OthogonalEfficiency
[ASRangeController] Inspect delegate's ASInterfaceState to delay preloading beyond viewport until visible.
2 parents 5737f31 + 984fe43 commit 005408b

10 files changed

Lines changed: 181 additions & 49 deletions

AsyncDisplayKit/ASCollectionNode.mm

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "ASCollectionNode.h"
1010
#import "ASCollectionInternal.h"
1111
#import "ASDisplayNode+Subclasses.h"
12+
#import "ASRangeController.h"
1213
#include <vector>
1314

1415
@interface _ASCollectionPendingState : NSObject
@@ -97,12 +98,12 @@ - (void)didLoad
9798
{
9899
[super didLoad];
99100

101+
ASCollectionView *view = self.view;
102+
view.collectionNode = self;
103+
100104
if (_pendingState) {
101105
_ASCollectionPendingState *pendingState = _pendingState;
102-
self.pendingState = nil;
103-
104-
ASCollectionView *view = self.view;
105-
view.collectionNode = self;
106+
self.pendingState = nil;
106107
view.asyncDelegate = pendingState.delegate;
107108
view.asyncDataSource = pendingState.dataSource;
108109
}
@@ -160,10 +161,13 @@ - (ASCollectionView *)view
160161
return (ASCollectionView *)[super view];
161162
}
162163

164+
#if RangeControllerLoggingEnabled
163165
- (void)visibilityDidChange:(BOOL)isVisible
164166
{
167+
[super visibilityDidChange:isVisible];
165168
NSLog(@"%@ - visible: %d", self, isVisible);
166169
}
170+
#endif
167171

168172
- (void)clearContents
169173
{

AsyncDisplayKit/ASCollectionView.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,11 @@ - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController
782782
return self.bounds.size;
783783
}
784784

785+
- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController
786+
{
787+
return self.collectionNode.interfaceState;
788+
}
789+
785790
- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths
786791
{
787792
return [_dataController nodesAtIndexPaths:indexPaths];
@@ -944,6 +949,27 @@ - (void)clearFetchedData
944949
}
945950
}
946951

952+
#pragma mark - _ASDisplayView behavior substitutions
953+
// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element.
954+
// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView.
955+
- (void)willMoveToWindow:(UIWindow *)newWindow
956+
{
957+
BOOL visible = (newWindow != nil);
958+
ASDisplayNode *node = self.collectionNode;
959+
if (visible && !node.inHierarchy) {
960+
[node __enterHierarchy];
961+
}
962+
}
963+
964+
- (void)didMoveToWindow
965+
{
966+
BOOL visible = (self.window != nil);
967+
ASDisplayNode *node = self.collectionNode;
968+
if (!visible && node.inHierarchy) {
969+
[node __exitHierarchy];
970+
}
971+
}
972+
947973
#pragma mark - UICollectionView dead-end intercepts
948974

949975
#if ASDISPLAYNODE_ASSERTIONS_ENABLED // Remove implementations entirely for efficiency if not asserting.

AsyncDisplayKit/ASDisplayNode.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
6666
ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay | ASInterfaceStateVisible,
6767
};
6868

69-
70-
7169
/**
7270
* An `ASDisplayNode` is an abstraction over `UIView` and `CALayer` that allows you to perform calculations about a view
7371
* hierarchy off the main thread, and could do rendering off the main thread as well.

AsyncDisplayKit/ASTableNode.m

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import "ASFlowLayoutController.h"
1010
#import "ASTableViewInternal.h"
1111
#import "ASDisplayNode+Subclasses.h"
12+
#import "ASRangeController.h"
1213

1314
@interface _ASTablePendingState : NSObject
1415
@property (weak, nonatomic) id <ASTableDelegate> delegate;
@@ -63,11 +64,12 @@ - (void)didLoad
6364
{
6465
[super didLoad];
6566

67+
ASTableView *view = self.view;
68+
view.tableNode = self;
69+
6670
if (_pendingState) {
6771
_ASTablePendingState *pendingState = _pendingState;
68-
self.pendingState = nil;
69-
70-
ASTableView *view = self.view;
72+
self.pendingState = nil;
7173
view.asyncDelegate = pendingState.delegate;
7274
view.asyncDataSource = pendingState.dataSource;
7375
}
@@ -125,6 +127,14 @@ - (ASTableView *)view
125127
return (ASTableView *)[super view];
126128
}
127129

130+
#if RangeControllerLoggingEnabled
131+
- (void)visibilityDidChange:(BOOL)isVisible
132+
{
133+
[super visibilityDidChange:isVisible];
134+
NSLog(@"%@ - visible: %d", self, isVisible);
135+
}
136+
#endif
137+
128138
- (void)clearContents
129139
{
130140
[super clearContents];

AsyncDisplayKit/ASTableView.mm

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ @interface ASTableView () <ASRangeControllerDataSource, ASRangeControllerDelegat
121121
// This also permits sharing logic with ASCollectionNode, as the superclass is not UIKit-controlled.
122122
@property (nonatomic, retain) ASTableNode *strongTableNode;
123123

124+
// Always set, whether ASCollectionView is created directly or via ASCollectionNode.
125+
@property (nonatomic, weak) ASTableNode *tableNode;
126+
124127
@end
125128

126129
@implementation ASTableView
@@ -700,6 +703,11 @@ - (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController
700703
return self.bounds.size;
701704
}
702705

706+
- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController
707+
{
708+
return self.tableNode.interfaceState;
709+
}
710+
703711
#pragma mark - ASRangeControllerDelegate
704712

705713
- (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController
@@ -943,4 +951,25 @@ - (void)clearFetchedData
943951
}
944952
}
945953

954+
#pragma mark - _ASDisplayView behavior substitutions
955+
// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element.
956+
// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView.
957+
- (void)willMoveToWindow:(UIWindow *)newWindow
958+
{
959+
BOOL visible = (newWindow != nil);
960+
ASDisplayNode *node = self.tableNode;
961+
if (visible && !node.inHierarchy) {
962+
[node __enterHierarchy];
963+
}
964+
}
965+
966+
- (void)didMoveToWindow
967+
{
968+
BOOL visible = (self.window != nil);
969+
ASDisplayNode *node = self.tableNode;
970+
if (!visible && node.inHierarchy) {
971+
[node __exitHierarchy];
972+
}
973+
}
974+
946975
@end

AsyncDisplayKit/ASTableViewInternal.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
@interface ASTableView (Internal)
1414

1515
@property (nonatomic, retain, readonly) ASDataController *dataController;
16+
@property (nonatomic, weak, readwrite) ASTableNode *tableNode;
1617

1718
/**
1819
* Initializer.

AsyncDisplayKit/Details/ASRangeController.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#import <AsyncDisplayKit/ASDataController.h>
1313
#import <AsyncDisplayKit/ASLayoutController.h>
1414

15+
#define RangeControllerLoggingEnabled 0
16+
1517
NS_ASSUME_NONNULL_BEGIN
1618

1719
@protocol ASRangeControllerDataSource;
@@ -102,6 +104,15 @@ NS_ASSUME_NONNULL_BEGIN
102104
*/
103105
- (CGSize)viewportSizeForRangeController:(ASRangeController *)rangeController;
104106

107+
/**
108+
* @param rangeController Sender.
109+
*
110+
* @returns the ASInterfaceState of the node that this controller is powering. This allows nested range controllers
111+
* to collaborate with one another, as an outer controller may set bits in .interfaceState such as Visible.
112+
* If this controller is an orthogonally scrolling element, it waits until it is visible to preload outside the viewport.
113+
*/
114+
- (ASInterfaceState)interfaceStateForRangeController:(ASRangeController *)rangeController;
115+
105116
- (NSArray *)rangeController:(ASRangeController *)rangeController nodesAtIndexPaths:(NSArray *)indexPaths;
106117

107118
- (ASDisplayNode *)rangeController:(ASRangeController *)rangeController nodeAtIndexPath:(NSIndexPath *)indexPath;

AsyncDisplayKit/Details/ASRangeControllerBeta.mm

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@
1717
#import "ASInternalHelpers.h"
1818
#import "ASDisplayNode+FrameworkPrivate.h"
1919

20+
extern BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState)
21+
{
22+
return ((interfaceState & ASInterfaceStateVisible) == ASInterfaceStateVisible);
23+
}
24+
25+
extern BOOL ASInterfaceStateIncludesDisplay(ASInterfaceState interfaceState)
26+
{
27+
return ((interfaceState & ASInterfaceStateDisplay) == ASInterfaceStateDisplay);
28+
}
29+
30+
extern BOOL ASInterfaceStateIncludesFetchData(ASInterfaceState interfaceState)
31+
{
32+
return ((interfaceState & ASInterfaceStateFetchData) == ASInterfaceStateFetchData);
33+
}
34+
2035
@interface ASRangeControllerBeta ()
2136
{
2237
BOOL _rangeIsValid;
@@ -79,52 +94,91 @@ - (void)_updateVisibleNodeIndexPaths
7994
[_layoutController setVisibleNodeIndexPaths:visibleNodePaths];
8095
}
8196

82-
NSSet *fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData];
83-
NSSet *displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay];
84-
NSSet *visibleIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible];
85-
86-
//NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
87-
//NSLog(@"visible sets are equal: %d", [visibleIndexPaths isEqualToSet:visibleNodePathsSet]);
97+
ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self];
8898

89-
// Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint.
90-
NSMutableSet *allIndexPaths = [fetchDataIndexPaths mutableCopy];
91-
[allIndexPaths unionSet:displayIndexPaths];
92-
[allIndexPaths unionSet:visibleIndexPaths];
99+
NSSet *visibleIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible];
93100

101+
#if RangeControllerLoggingEnabled
94102
NSMutableArray *modified = [NSMutableArray array];
103+
#endif
104+
105+
if (ASInterfaceStateIncludesVisible(selfInterfaceState)) {
106+
// If we are already visible, get busy! Better get started on preloading before the user scrolls more...
107+
NSSet *fetchDataIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeFetchData];
108+
NSSet *displayIndexPaths = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeDisplay];
95109

96-
for (NSIndexPath *indexPath in allIndexPaths) {
97-
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
98-
// For consistency, make sure each node knows that it should measure itself if something changes.
99-
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout;
110+
// Typically the fetchDataIndexPaths will be the largest, and be a superset of the others, though it may be disjoint.
111+
NSMutableSet *allIndexPaths = [fetchDataIndexPaths mutableCopy];
112+
[allIndexPaths unionSet:displayIndexPaths];
113+
[allIndexPaths unionSet:visibleIndexPaths];
100114

101-
if ([fetchDataIndexPaths containsObject:indexPath]) {
102-
interfaceState |= ASInterfaceStateFetchData;
103-
}
104-
if ([displayIndexPaths containsObject:indexPath]) {
105-
interfaceState |= ASInterfaceStateDisplay;
106-
}
107-
if ([visibleIndexPaths containsObject:indexPath]) {
108-
interfaceState |= ASInterfaceStateVisible;
115+
for (NSIndexPath *indexPath in allIndexPaths) {
116+
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
117+
// For consistency, make sure each node knows that it should measure itself if something changes.
118+
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout;
119+
120+
if ([fetchDataIndexPaths containsObject:indexPath]) {
121+
interfaceState |= ASInterfaceStateFetchData;
122+
}
123+
if ([displayIndexPaths containsObject:indexPath]) {
124+
interfaceState |= ASInterfaceStateDisplay;
125+
}
126+
if ([visibleIndexPaths containsObject:indexPath]) {
127+
interfaceState |= ASInterfaceStateVisible;
128+
}
129+
130+
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
131+
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
132+
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
133+
if (node.interfaceState != interfaceState) {
134+
#if RangeControllerLoggingEnabled
135+
[modified addObject:indexPath];
136+
#endif
137+
[node recursivelySetInterfaceState:interfaceState];
138+
}
109139
}
140+
} else {
141+
// If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the
142+
// instant we come onscreen. So, fetch data and display all of those things, but don't waste resources preloading yet.
143+
// We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:.
110144

111-
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
112-
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
113-
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
114-
if (node.interfaceState != interfaceState) {
115-
[modified addObject:indexPath];
116-
[node recursivelySetInterfaceState:interfaceState];
145+
for (NSIndexPath *indexPath in visibleIndexPaths) {
146+
// Set Layout, Fetch Data, Display. DO NOT set Visible: even though these elements are in the visible range / "viewport",
147+
// our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above.
148+
ASInterfaceState interfaceState = ASInterfaceStateMeasureLayout | ASInterfaceStateFetchData | ASInterfaceStateDisplay;
149+
150+
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
151+
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
152+
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
153+
if (node.interfaceState != interfaceState) {
154+
#if RangeControllerLoggingEnabled
155+
[modified addObject:indexPath];
156+
#endif
157+
[node recursivelySetInterfaceState:interfaceState];
158+
}
117159
}
118160
}
119161

120-
/*
162+
#if RangeControllerLoggingEnabled
163+
NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
164+
BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet];
165+
NSLog(@"visible sets are equal: %d", setsAreEqual);
166+
if (!setsAreEqual) {
167+
NSLog(@"standard: %@", visibleIndexPaths);
168+
NSLog(@"custom: %@", visibleNodePathsSet);
169+
}
170+
121171
[modified sortUsingSelector:@selector(compare:)];
122172

123173
for (NSIndexPath *indexPath in modified) {
124-
NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, [visibleIndexPaths containsObject:indexPath], [displayIndexPaths containsObject:indexPath], [fetchDataIndexPaths containsObject:indexPath]);
174+
ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath];
175+
ASInterfaceState interfaceState = node.interfaceState;
176+
BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState);
177+
BOOL inDisplay = ASInterfaceStateIncludesDisplay(interfaceState);
178+
BOOL inFetchData = ASInterfaceStateIncludesFetchData(interfaceState);
179+
NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, inVisible, inDisplay, inFetchData);
125180
}
126-
*/
127-
181+
#endif
128182

129183
_rangeIsValid = YES;
130184
_queuedRangeUpdate = NO;

AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState)
6666
- (void)enterHierarchyState:(ASHierarchyState)hierarchyState;
6767
- (void)exitHierarchyState:(ASHierarchyState)hierarchyState;
6868

69+
// Changed before calling willEnterHierarchy / didExitHierarchy.
70+
@property (nonatomic, readwrite, assign, getter = isInHierarchy) BOOL inHierarchy;
71+
// Call willEnterHierarchy if necessary and set inHierarchy = YES if visibility notifications are enabled on all of its parents
72+
- (void)__enterHierarchy;
73+
// Call didExitHierarchy if necessary and set inHierarchy = NO if visibility notifications are enabled on all of its parents
74+
- (void)__exitHierarchy;
75+
6976
/**
7077
* @abstract Returns the Hierarchy State of the node.
7178
*

AsyncDisplayKit/Private/ASDisplayNodeInternal.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,20 +132,12 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
132132
- (void)__layout;
133133
- (void)__setSupernode:(ASDisplayNode *)supernode;
134134

135-
// Changed before calling willEnterHierarchy / didExitHierarchy.
136-
@property (nonatomic, readwrite, assign, getter = isInHierarchy) BOOL inHierarchy;
137-
138135
// Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this.
139136
- (BOOL)__visibilityNotificationsDisabled;
140137
- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled;
141138
- (void)__incrementVisibilityNotificationsDisabled;
142139
- (void)__decrementVisibilityNotificationsDisabled;
143140

144-
// Call willEnterHierarchy if necessary and set inHierarchy = YES if visibility notifications are enabled on all of its parents
145-
- (void)__enterHierarchy;
146-
// Call didExitHierarchy if necessary and set inHierarchy = NO if visibility notifications are enabled on all of its parents
147-
- (void)__exitHierarchy;
148-
149141
// Helper method to summarize whether or not the node run through the display process
150142
- (BOOL)__implementsDisplay;
151143

0 commit comments

Comments
 (0)