Fix double-load issue with ASCollectionNode (#372)

* Fix double-reload issue with ASCollectionNode

* Add a message to the change pile

* Fix some license headers

* Address feedback
This commit is contained in:
Adlai Holler 2017-06-19 14:18:56 -07:00 committed by GitHub
parent 4829a8643d
commit 326925839d
9 changed files with 74 additions and 92 deletions

View File

@ -9,6 +9,7 @@
- [Yoga] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga-powered layout calculation. [Scott Goodson](https://github.com/appleguy) - [Yoga] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga-powered layout calculation. [Scott Goodson](https://github.com/appleguy)
- Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon) - Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon)
- Migrated unit tests to OCMock 3.4 (from 2.2) and improved the multiplex image node tests. [Adlai Holler](https://github.com/Adlai-Holler) - Migrated unit tests to OCMock 3.4 (from 2.2) and improved the multiplex image node tests. [Adlai Holler](https://github.com/Adlai-Holler)
- Fix CollectionNode double-load issue. This should significantly improve performance in cases where a collection node has content immediately available on first layout i.e. not fetched from the network. [Adlai Holler](https://github.com/Adlai-Holler)
## 2.3.3 ## 2.3.3
- [ASTextKitFontSizeAdjuster] Replace use of NSAttributedString's boundingRectWithSize:options:context: with NSLayoutManager's boundingRectForGlyphRange:inTextContainer: [Ricky Cancro](https://github.com/rcancro) - [ASTextKitFontSizeAdjuster] Replace use of NSAttributedString's boundingRectWithSize:options:context: with NSLayoutManager's boundingRectForGlyphRange:inTextContainer: [Ricky Cancro](https://github.com/rcancro)

View File

@ -28,6 +28,7 @@
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h> #import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASInternalHelpers.h> #import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASCellNode+Internal.h> #import <AsyncDisplayKit/ASCellNode+Internal.h>
#import <AsyncDisplayKit/_ASHierarchyChangeSet.h>
#import <AsyncDisplayKit/AsyncDisplayKit+Debug.h> #import <AsyncDisplayKit/AsyncDisplayKit+Debug.h>
#import <AsyncDisplayKit/ASSectionContext.h> #import <AsyncDisplayKit/ASSectionContext.h>
#import <AsyncDisplayKit/ASDataController.h> #import <AsyncDisplayKit/ASDataController.h>
@ -682,9 +683,17 @@
- (void)reloadDataWithCompletion:(void (^)())completion - (void)reloadDataWithCompletion:(void (^)())completion
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (self.nodeLoaded) { if (!self.nodeLoaded) {
[self.view reloadDataWithCompletion:completion]; return;
} }
[self performBatchUpdates:^{
[self.view.changeSet reloadData];
} completion:^(BOOL finished){
if (completion) {
completion();
}
}];
} }
- (void)reloadData - (void)reloadData
@ -692,14 +701,19 @@
[self reloadDataWithCompletion:nil]; [self reloadDataWithCompletion:nil];
} }
- (void)relayoutItems
{
[self.view relayoutItems];
}
- (void)reloadDataImmediately - (void)reloadDataImmediately
{ {
[self.view reloadDataImmediately]; ASDisplayNodeAssertMainThread();
[self reloadData];
[self waitUntilAllUpdatesAreCommitted];
}
- (void)relayoutItems
{
ASDisplayNodeAssertMainThread();
if (self.nodeLoaded) {
[self.view relayoutItems];
}
} }
- (void)beginUpdates - (void)beginUpdates

View File

@ -266,14 +266,14 @@ NS_ASSUME_NONNULL_BEGIN
* the main thread. * the main thread.
* @warning This method is substantially more expensive than UICollectionView's version. * @warning This method is substantially more expensive than UICollectionView's version.
*/ */
- (void)reloadDataWithCompletion:(nullable void (^)())completion ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); - (void)reloadDataWithCompletion:(nullable void (^)())completion AS_UNAVAILABLE("Use ASCollectionNode method instead.");
/** /**
* Reload everything from scratch, destroying the working range and all cached nodes. * Reload everything from scratch, destroying the working range and all cached nodes.
* *
* @warning This method is substantially more expensive than UICollectionView's version. * @warning This method is substantially more expensive than UICollectionView's version.
*/ */
- (void)reloadData ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode method instead."); - (void)reloadData AS_UNAVAILABLE("Use ASCollectionNode method instead.");
/** /**
* Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes. * Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes.
@ -281,7 +281,7 @@ NS_ASSUME_NONNULL_BEGIN
* @warning This method is substantially more expensive than UICollectionView's version and will block the main thread * @warning This method is substantially more expensive than UICollectionView's version and will block the main thread
* while all the cells load. * while all the cells load.
*/ */
- (void)reloadDataImmediately ASDISPLAYNODE_DEPRECATED_MSG("Use ASCollectionNode's -reloadDataWithCompletion: followed by -waitUntilAllUpdatesAreCommitted instead."); - (void)reloadDataImmediately AS_UNAVAILABLE("Use ASCollectionNode method instead.");
/** /**
* Triggers a relayout of all nodes. * Triggers a relayout of all nodes.

View File

@ -145,11 +145,6 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
*/ */
BOOL _hasEverCheckedForBatchFetchingDueToUpdate; BOOL _hasEverCheckedForBatchFetchingDueToUpdate;
/**
* The change set that we're currently building, if any.
*/
_ASHierarchyChangeSet *_changeSet;
/** /**
* Counter used to keep track of nested batch updates. * Counter used to keep track of nested batch updates.
*/ */
@ -333,31 +328,23 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
#pragma mark - #pragma mark -
#pragma mark Overrides. #pragma mark Overrides.
- (void)reloadDataWithCompletion:(void (^)())completion /**
{ * This method is not available to be called by the public i.e.
ASDisplayNodeAssertMainThread(); * it should only be called by UICollectionView itself. UICollectionView
* does this e.g. during the first layout pass, or if you call -numberOfSections
if (! _dataController.initialReloadDataHasBeenCalled) { * before its content is loaded.
// If this is the first reload, forward to super immediately to prevent it from triggering more "initial" loads while our data controller is working. */
_superIsPendingDataLoad = YES;
[super reloadData];
}
void (^batchUpdatesCompletion)(BOOL);
if (completion) {
batchUpdatesCompletion = ^(BOOL) {
completion();
};
}
[self performBatchUpdates:^{
[_changeSet reloadData];
} completion:batchUpdatesCompletion];
}
- (void)reloadData - (void)reloadData
{ {
[self reloadDataWithCompletion:nil]; [super reloadData];
// UICollectionView calls -reloadData during first layoutSubviews and when the data source changes.
// This fires off the first load of cell nodes.
if (_asyncDataSource != nil && !self.dataController.initialReloadDataHasBeenCalled) {
[self performBatchUpdates:^{
[_changeSet reloadData];
} completion:nil];
}
} }
- (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated - (void)scrollToItemAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UICollectionViewScrollPosition)scrollPosition animated:(BOOL)animated
@ -367,13 +354,6 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
} }
} }
- (void)reloadDataImmediately
{
ASDisplayNodeAssertMainThread();
[self reloadData];
[self waitUntilAllUpdatesAreCommitted];
}
- (void)relayoutItems - (void)relayoutItems
{ {
[_dataController relayoutAllNodes]; [_dataController relayoutAllNodes];
@ -1721,7 +1701,6 @@ static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier";
} }
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath];
return CGSizeEqualToSizeWithIn(attributes.size, size, FLT_EPSILON); return CGSizeEqualToSizeWithIn(attributes.size, size, FLT_EPSILON);
} }
#pragma mark - ASDataControllerSource optional methods #pragma mark - ASDataControllerSource optional methods

View File

@ -31,6 +31,11 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, strong, readonly) ASDataController *dataController; @property (nonatomic, strong, readonly) ASDataController *dataController;
@property (nonatomic, strong, readonly) ASRangeController *rangeController; @property (nonatomic, strong, readonly) ASRangeController *rangeController;
/**
* The change set that we're currently building, if any.
*/
@property (nonatomic, strong, nullable, readonly) _ASHierarchyChangeSet *changeSet;
/** /**
* @see ASCollectionNode+Beta.h for full documentation. * @see ASCollectionNode+Beta.h for full documentation.
*/ */

View File

@ -163,30 +163,6 @@ NS_ASSUME_NONNULL_BEGIN
*/ */
- (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion; - (void)performBatchUpdates:(nullable AS_NOESCAPE void (^)())updates completion:(nullable void (^)(BOOL finished))completion;
/**
* Reload everything from scratch, destroying the working range and all cached nodes.
*
* @param completion block to run on completion of asynchronous loading or nil. If supplied, the block is run on
* the main thread.
* @warning This method is substantially more expensive than UICollectionView's version.
*/
- (void)reloadDataWithCompletion:(nullable void (^)())completion;
/**
* Reload everything from scratch, destroying the working range and all cached nodes.
*
* @warning This method is substantially more expensive than UICollectionView's version.
*/
- (void)reloadData;
/**
* Reload everything from scratch entirely on the main thread, destroying the working range and all cached nodes.
*
* @warning This method is substantially more expensive than UICollectionView's version and will block the main thread
* while all the cells load.
*/
- (void)reloadDataImmediately;
/** /**
* Triggers a relayout of all nodes. * Triggers a relayout of all nodes.
* *

View File

@ -151,10 +151,15 @@
- (void)loadInitialData - (void)loadInitialData
{ {
/// BUG: these methods are called twice in a row i.e. this for-loop shouldn't be here. https://github.com/TextureGroup/Texture/issues/351 // Count methods are called twice in a row for first data load.
// Since -reloadData is routed through our batch update system,
// the batch update latches the "old data source counts" if needed at -beginUpdates time
// and then verifies them against the "new data source counts" after the updates.
// This isn't ideal, but the cost is very small and the system works well.
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
// It reads all the counts // It reads all the counts
[self expectDataSourceCountMethods]; [self expectDataSourceCountMethods];
}
// It reads each section object. // It reads each section object.
for (NSInteger section = 0; section < sections.count; section++) { for (NSInteger section = 0; section < sections.count; section++) {
@ -172,7 +177,6 @@
[self expectNodeBlockMethodForItemAtIndexPath:indexPath]; [self expectNodeBlockMethodForItemAtIndexPath:indexPath];
} }
} }
}
[window layoutIfNeeded]; [window layoutIfNeeded];

View File

@ -1,5 +1,5 @@
// //
// ASCollectionViewTests.m // ASCollectionViewTests.mm
// Texture // Texture
// //
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
@ -259,7 +259,8 @@
[window setRootViewController:testController]; [window setRootViewController:testController];
[window makeKeyAndVisible]; [window makeKeyAndVisible];
[testController.collectionView reloadDataImmediately]; [testController.collectionNode reloadData];
[testController.collectionNode waitUntilAllUpdatesAreCommitted];
[testController.collectionView layoutIfNeeded]; [testController.collectionView layoutIfNeeded];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
@ -390,12 +391,13 @@
ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];\ ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil];\
__unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\ __unused ASCollectionViewTestDelegate *del = testController.asyncDelegate;\
__unused ASCollectionView *cv = testController.collectionView;\ __unused ASCollectionView *cv = testController.collectionView;\
__unused ASCollectionNode *cn = testController.collectionNode;\ ASCollectionNode *cn = testController.collectionNode;\
UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\ UIWindow *window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];\
[window makeKeyAndVisible]; \ [window makeKeyAndVisible]; \
window.rootViewController = testController;\ window.rootViewController = testController;\
\ \
[testController.collectionView reloadDataImmediately];\ [cn reloadData];\
[cn waitUntilAllUpdatesAreCommitted]; \
[testController.collectionView layoutIfNeeded]; [testController.collectionView layoutIfNeeded];
- (void)testThatSubmittingAValidInsertDoesNotThrowAnException - (void)testThatSubmittingAValidInsertDoesNotThrowAnException
@ -620,7 +622,7 @@
for (NSInteger i = 0; i < 2; i++) { for (NSInteger i = 0; i < 2; i++) {
// NOTE: waitUntilAllUpdatesAreCommitted or reloadDataImmediately is not sufficient here!! // NOTE: waitUntilAllUpdatesAreCommitted or reloadDataImmediately is not sufficient here!!
XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]]; XCTestExpectation *done = [self expectationWithDescription:[NSString stringWithFormat:@"Reload #%td complete", i]];
[cv reloadDataWithCompletion:^{ [cn reloadDataWithCompletion:^{
[done fulfill]; [done fulfill];
}]; }];
[self waitForExpectationsWithTimeout:1 handler:nil]; [self waitForExpectationsWithTimeout:1 handler:nil];
@ -752,7 +754,8 @@
updateValidationTestPrologue updateValidationTestPrologue
del.sectionGeneration++; del.sectionGeneration++;
[cv reloadDataImmediately]; [cn reloadData];
[cn waitUntilAllUpdatesAreCommitted];
NSInteger sectionCount = del->_itemCounts.size(); NSInteger sectionCount = del->_itemCounts.size();
for (NSInteger section = 0; section < sectionCount; section++) { for (NSInteger section = 0; section < sectionCount; section++) {

View File

@ -1,5 +1,5 @@
// //
// ASDisplayNodeLayoutTests.m // ASDisplayNodeLayoutTests.mm
// Texture // Texture
// //
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.