Fixes crash when accessing a cell within working range updates

Summary:
I was able to build a unit test that reproduces the issue. We can avoid the crash by simply returning `nil` when accessing a cell while working range events are being vended.

There is definitely something weird going on here though. When debugging `cellForItemAtIndexPath:` I found:

```
(lldb) po indexPath
<NSIndexPath: 0xc000000000000516> {length = 2, path = 5 - 0}
(lldb) po [[self collectionView] numberOfSections]
11
```

So in theory we should be fine, right? But when I continue I get

```
*** Assertion failure in -[UICollectionViewData numberOfItemsBeforeSection:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3600.5.2/UICollectionViewData.m:611
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'request for number of items before section 5 when there are only 3 sections in the collection view'
```

There _were_ 3 sections in the UICV before the update, but the data source and structure powering
Closes https://github.com/Instagram/IGListKit/pull/216

Differential Revision: D4204625

Pulled By: rnystrom

fbshipit-source-id: 455ed199dfc115077e4294e2843016a50e179015
This commit is contained in:
Ryan Nystrom 2016-11-18 09:55:17 -08:00 committed by Facebook Github Bot
parent cb8e0aba4a
commit 74affe0887
6 changed files with 56 additions and 6 deletions

View file

@ -28,6 +28,10 @@ This release closes the [2.0.0 milestone](https://github.com/Instagram/IGListKit
- Added new `-[IGListAdapter visibleObjects]` API. [Ryan Nystrom](https://github.com/rnystrom) [(386ae07)](https://github.com/Instagram/IGListKit/commit/386ae0786445c06e1eabf074a4181614332f155f)
- Added new `-[IGListAdapter objectForSectionController:]` API. [Ayush Saraswat](https://github.com/saraswatayu) [(#204)](https://github.com/Instagram/IGListKit/pull/204)
### Fixes
- Prevent `UICollectionView` bug when accessing a cell during working range updates. [Ryan Nystrom](https://github.com/rnystrom) [(#216)](https://github.com/Instagram/IGListKit/pull/216)
1.0.0
-----

View file

@ -19,6 +19,7 @@
@implementation IGListAdapter {
NSMapTable<UICollectionViewCell *, IGListSectionController<IGListSectionType> *> *_cellSectionControllerMap;
BOOL _isDequeuingCell;
BOOL _isSendingWorkingRangeDisplayUpdates;
}
- (void)dealloc {
@ -613,7 +614,10 @@
id object = [self.sectionMap objectForSection:indexPath.section];
[self.displayHandler willDisplayCell:cell forListAdapter:self sectionController:sectionController object:object indexPath:indexPath];
_isSendingWorkingRangeDisplayUpdates = YES;
[self.workingRangeHandler willDisplayItemAtIndexPath:indexPath forListAdapter:self];
_isSendingWorkingRangeDisplayUpdates = NO;
}
- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
@ -691,8 +695,8 @@
IGAssertMainThread();
IGParameterAssert(sectionController != nil);
// if this is accessed while a cell is being dequeued, just return nil
if (_isDequeuingCell) {
// if this is accessed while a cell is being dequeued or displaying working range elements, just return nil
if (_isDequeuingCell || _isSendingWorkingRangeDisplayUpdates) {
return nil;
}

View file

@ -1022,4 +1022,37 @@
[self waitForExpectationsWithTimeout:15 handler:nil];
}
- (void)test_whenPerformingUpdates_withWorkingRange_thatAccessingCellDoesntCrash {
[self setupWithObjects:@[
genTestObject(@1, @1),
genTestObject(@2, @1),
genTestObject(@3, @1),
]];
// section controller try to access a cell in -listAdapter:sectionControllerWillEnterWorkingRange:
// add items beyond the 100x100 frame so they access unavailable cells
self.dataSource.objects = @[
genTestObject(@1, @1),
genTestObject(@2, @1),
genTestObject(@3, @1),
genTestObject(@4, @1),
genTestObject(@5, @1),
genTestObject(@6, @1),
genTestObject(@7, @1),
genTestObject(@8, @1),
genTestObject(@9, @1),
genTestObject(@10, @1),
genTestObject(@11, @1),
];
XCTestExpectation *expectation = genExpectation;
// this will call -collectionView:performBatchUpdates:, trigger collectionView:willDisplayCell:forItemAtIndexPath:,
// which kicks off the working range logic
[self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
[expectation fulfill];
}];
[self waitForExpectationsWithTimeout:15 handler:nil];
}
@end

View file

@ -3,7 +3,7 @@
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
@ -48,7 +48,7 @@
}
- (IGListSectionController<IGListSectionType> *)listAdapter:(IGListAdapter *)listAdapter
sectionControllerForObject:(id)object {
sectionControllerForObject:(id)object {
return [_map objectForKey:object];
}
@ -211,7 +211,7 @@
// Act: Hide the first item, and watch for the second item to leave the working range.
[[mockWorkingRangeDelegate expect] listAdapter:adapter sectionControllerDidExitWorkingRange:controller2];
[adapter.workingRangeHandler didEndDisplayingItemAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] forListAdapter:adapter];
[mockWorkingRangeDelegate verifyWithDelay:5];
}

View file

@ -13,7 +13,7 @@
@class IGTestObject;
@interface IGTestDelegateController : IGListSectionController <IGListSectionType, IGListDisplayDelegate>
@interface IGTestDelegateController : IGListSectionController <IGListSectionType, IGListDisplayDelegate, IGListWorkingRangeDelegate>
@property (nonatomic, strong, readonly) IGTestObject *item;

View file

@ -18,6 +18,7 @@
if (self = [super init]) {
_willDisplayCellIndexes = [NSCountedSet new];
_didEndDisplayCellIndexes = [NSCountedSet new];
self.workingRangeDelegate = self;
}
return self;
}
@ -81,4 +82,12 @@
- (void)listAdapter:(IGListAdapter *)listAdapter didScrollSectionController:(IGListSectionController <IGListSectionType> *)sectionController {}
#pragma mark - IGListWorkingRangeDelegate
- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerWillEnterWorkingRange:(IGListSectionController<IGListSectionType> *)sectionController {
__unused UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self];
}
- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerDidExitWorkingRange:(IGListSectionController<IGListSectionType> *)sectionController {}
@end