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

Commit eea5dda

Browse files
author
Adlai Holler
authored
Range Controller Explicitly Marks Disappeared Nodes as Invisible (#2805)
* Improve visibility handling for range-managed nodes * Tweak ASWeakSet & the test * Put back a few things that are pending a different diff * Add a test * Ensure we update visible nodes, even if there are no new ones
1 parent 64ee8db commit eea5dda

4 files changed

Lines changed: 56 additions & 4 deletions

File tree

AsyncDisplayKit/Details/ASRangeController.mm

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ @interface ASRangeController ()
3434
BOOL _layoutControllerImplementsSetVisibleIndexPaths;
3535
BOOL _layoutControllerImplementsSetViewportSize;
3636
NSSet<NSIndexPath *> *_allPreviousIndexPaths;
37+
ASWeakSet<ASCellNode *> *_visibleNodes;
3738
ASLayoutRangeMode _currentRangeMode;
3839
BOOL _preserveCurrentRangeMode;
3940
BOOL _didRegisterForNodeDisplayNotifications;
@@ -169,6 +170,24 @@ - (void)setDataSource:(id<ASRangeControllerDataSource>)dataSource
169170
}
170171
}
171172

173+
// Clear the visible bit from any nodes that disappeared since last update.
174+
// Currently we guarantee that nodes will not be marked visible when deallocated,
175+
// but it's OK to be in e.g. the preload range. So for the visible bit specifically,
176+
// we add this extra mechanism to account for e.g. deleted items.
177+
//
178+
// NOTE: There is a minor risk here, if a node is transferred from one range controller
179+
// to another before the first rc updates and clears the node out of this set. It's a pretty
180+
// wild scenario that I doubt happens in practice.
181+
- (void)_setVisibleNodes:(ASWeakSet *)newVisibleNodes
182+
{
183+
for (ASCellNode *node in _visibleNodes) {
184+
if (![newVisibleNodes containsObject:node] && node.isVisible) {
185+
[node exitInterfaceState:ASInterfaceStateVisible];
186+
}
187+
}
188+
_visibleNodes = newVisibleNodes;
189+
}
190+
172191
- (void)_updateVisibleNodeIndexPaths
173192
{
174193
ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController");
@@ -187,8 +206,10 @@ - (void)_updateVisibleNodeIndexPaths
187206
// TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges
188207
// Example: ... = [_layoutController indexPathsForScrolling:scrollDirection rangeType:ASLayoutRangeTypeVisible];
189208
NSArray<NSIndexPath *> *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self];
190-
209+
ASWeakSet *newVisibleNodes = [[ASWeakSet alloc] init];
210+
191211
if (visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)...
212+
[self _setVisibleNodes:newVisibleNodes];
192213
return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later
193214
}
194215
ASProfilingSignpostStart(1, self);
@@ -272,7 +293,7 @@ - (void)_updateVisibleNodeIndexPaths
272293
ASDisplayNodeAssertTrue([visibleIndexPaths isSubsetOfSet:displayIndexPaths]);
273294
NSMutableArray<NSIndexPath *> *modifiedIndexPaths = (ASRangeControllerLoggingEnabled ? [NSMutableArray array] : nil);
274295
#endif
275-
296+
276297
for (NSIndexPath *indexPath in allIndexPaths) {
277298
// Before a node / indexPath is exposed to ASRangeController, ASDataController should have already measured it.
278299
// For consistency, make sure each node knows that it should measure itself if something changes.
@@ -326,6 +347,9 @@ - (void)_updateVisibleNodeIndexPaths
326347
ASDisplayNode *node = currentSectionNodes[row];
327348

328349
ASDisplayNodeAssert(node.hierarchyState & ASHierarchyStateRangeManaged, @"All nodes reaching this point should be range-managed, or interfaceState may be incorrectly reset.");
350+
if (ASInterfaceStateIncludesVisible(interfaceState)) {
351+
[newVisibleNodes addObject:node];
352+
}
329353
// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState.
330354
if (node.interfaceState != interfaceState) {
331355
#if ASRangeControllerLoggingEnabled
@@ -345,6 +369,8 @@ - (void)_updateVisibleNodeIndexPaths
345369
}
346370
}
347371
}
372+
373+
[self _setVisibleNodes:newVisibleNodes];
348374

349375
// TODO: This code is for debugging only, but would be great to clean up with a delegate method implementation.
350376
if ([ASRangeController shouldShowRangeDebugOverlay]) {

AsyncDisplayKit/Details/ASWeakSet.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515

1616
NS_ASSUME_NONNULL_BEGIN
1717

18+
/**
19+
* A class similar to NSSet that stores objects weakly.
20+
* Note that this class uses NSPointerFunctionsObjectPointerPersonality –
21+
* that is, it uses shifted pointer for hashing, and identity comparison for equality.
22+
*/
1823
@interface ASWeakSet<__covariant ObjectType> : NSObject<NSFastEnumeration>
1924

2025
/// Returns YES if the receiver is empty, NO otherwise.

AsyncDisplayKit/Details/ASWeakSet.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ - (instancetype)init
2222
{
2323
self = [super init];
2424
if (self) {
25-
_hashTable = [NSHashTable weakObjectsHashTable];
25+
_hashTable = [NSHashTable hashTableWithOptions:NSPointerFunctionsWeakMemory | NSPointerFunctionsObjectPointerPersonality];
2626
}
2727
return self;
2828
}
@@ -59,7 +59,7 @@ - (BOOL)isEmpty
5959

6060
/**
6161
Note: The `count` property of NSHashTable is unreliable
62-
in the case of weak-object hash tables because entries
62+
in the case of weak-memory hash tables because entries
6363
that have been deallocated are not removed immediately.
6464
6565
In order to get the true count we have to fall back to using

AsyncDisplayKitTests/ASCollectionViewTests.mm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,27 @@ - (void)testThatNilBatchUpdatesCanBeSubmitted
815815
[cn performBatchAnimated:NO updates:nil completion:nil];
816816
}
817817

818+
- (void)testThatDeletedItemsAreMarkedInvisible
819+
{
820+
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
821+
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];
822+
window.rootViewController = testController;
823+
824+
__block NSInteger itemCount = 1;
825+
testController.asyncDelegate->_itemCounts = {itemCount};
826+
[window makeKeyAndVisible];
827+
[window layoutIfNeeded];
828+
829+
ASCollectionNode *cn = testController.collectionNode;
830+
[cn waitUntilAllUpdatesAreCommitted];
831+
ASCellNode *node = [cn nodeForItemAtIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
832+
XCTAssertTrue(node.visible);
833+
testController.asyncDelegate->_itemCounts = {0};
834+
[cn deleteItemsAtIndexPaths: @[[NSIndexPath indexPathForItem:0 inSection:0]]];
835+
[self expectationForPredicate:[NSPredicate predicateWithFormat:@"visible = NO"] evaluatedWithObject:node handler:nil];
836+
[self waitForExpectationsWithTimeout:3 handler:nil];
837+
}
838+
818839
- (void)testThatMultipleBatchFetchesDontHappenUnnecessarily
819840
{
820841
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];

0 commit comments

Comments
 (0)