ASCollectionView: Suspend updates while reload data is pending

This commit is contained in:
Adlai Holler 2015-09-19 13:35:57 -07:00
parent 4e86ca2024
commit 77490e11c0

View File

@ -131,6 +131,15 @@ static BOOL _isInterceptedSelector(SEL sel)
@property (atomic, assign) BOOL asyncDataSourceLocked; @property (atomic, assign) BOOL asyncDataSourceLocked;
/**
If YES, the `UICollectionView` will reload its data on next `layoutSubviews` so we should
not forward any updates to it.
Rationale:
In `reloadData`, a collection view invalidates its data and marks itself as needing reload, and waits until `layoutSubviews` to requery its data source. This can lead to data inconsistency problems. Say you have an empty collection view. You call `reloadData`, then immediately insert an item into your data source and call `insertItemsAtIndexPaths:[0,0]`. You will get an assertion failure saying "Invalid number of items in section 0. The number of items after the update (1) must be equal to the number of items before the update (1) plus or minus the items added and removed (1 added, 0 removed)." The collection view never queried your data source before the update to see that it actually had 0 items.
*/
@property (nonatomic) BOOL pendingReloadData;
@end @end
@implementation ASCollectionView @implementation ASCollectionView
@ -207,6 +216,7 @@ static BOOL _isInterceptedSelector(SEL sel)
ASDisplayNodeAssert(self.asyncDelegate, @"ASCollectionView's asyncDelegate property must be set."); ASDisplayNodeAssert(self.asyncDelegate, @"ASCollectionView's asyncDelegate property must be set.");
ASDisplayNodePerformBlockOnMainThread(^{ ASDisplayNodePerformBlockOnMainThread(^{
[super reloadData]; [super reloadData];
self.pendingReloadData = YES;
}); });
[_dataController reloadDataWithAnimationOptions:kASCollectionViewAnimationNone completion:completion]; [_dataController reloadDataWithAnimationOptions:kASCollectionViewAnimationNone completion:completion];
} }
@ -403,6 +413,7 @@ static BOOL _isInterceptedSelector(SEL sel)
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{ {
self.pendingReloadData = NO;
return [_dataController numberOfSections]; return [_dataController numberOfSections];
} }
@ -485,7 +496,7 @@ static BOOL _isInterceptedSelector(SEL sel)
- (void)layoutSubviews - (void)layoutSubviews
{ {
if (! CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, self.bounds.size)) { if (!self.pendingReloadData && ! CGSizeEqualToSize(_maxSizeForNodesConstrainedSize, self.bounds.size)) {
_maxSizeForNodesConstrainedSize = self.bounds.size; _maxSizeForNodesConstrainedSize = self.bounds.size;
// First size change occurs during initial configuration. An expensive relayout pass is unnecessary at that time // First size change occurs during initial configuration. An expensive relayout pass is unnecessary at that time
@ -638,7 +649,7 @@ static BOOL _isInterceptedSelector(SEL sel)
- (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { - (void)rangeController:(ASRangeController *)rangeController endUpdatesAnimated:(BOOL)animated completion:(void (^)(BOOL))completion {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) { if (!self.asyncDataSource || self.pendingReloadData) {
if (completion) { if (completion) {
completion(NO); completion(NO);
} }
@ -690,7 +701,7 @@ static BOOL _isInterceptedSelector(SEL sel)
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) { if (!self.asyncDataSource || self.pendingReloadData) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
@ -709,7 +720,7 @@ static BOOL _isInterceptedSelector(SEL sel)
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) { if (!self.asyncDataSource || self.pendingReloadData) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
@ -728,7 +739,7 @@ static BOOL _isInterceptedSelector(SEL sel)
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) { if (!self.asyncDataSource || self.pendingReloadData) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }
@ -747,7 +758,7 @@ static BOOL _isInterceptedSelector(SEL sel)
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!self.asyncDataSource) { if (!self.asyncDataSource || self.pendingReloadData) {
return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes return; // if the asyncDataSource has become invalid while we are processing, ignore this request to avoid crashes
} }