diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bcd804..896a0095 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ----- diff --git a/Source/IGListAdapter.m b/Source/IGListAdapter.m index 2ed4a346..574739a1 100644 --- a/Source/IGListAdapter.m +++ b/Source/IGListAdapter.m @@ -19,6 +19,7 @@ @implementation IGListAdapter { NSMapTable *> *_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; } diff --git a/Tests/IGListAdapterE2ETests.m b/Tests/IGListAdapterE2ETests.m index 4f2cb01c..3499af06 100644 --- a/Tests/IGListAdapterE2ETests.m +++ b/Tests/IGListAdapterE2ETests.m @@ -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 diff --git a/Tests/IGListWorkingRangeHandlerTests.m b/Tests/IGListWorkingRangeHandlerTests.m index eddf7735..9a5cfc1e 100644 --- a/Tests/IGListWorkingRangeHandlerTests.m +++ b/Tests/IGListWorkingRangeHandlerTests.m @@ -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 *)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]; } diff --git a/Tests/Objects/IGTestDelegateController.h b/Tests/Objects/IGTestDelegateController.h index adc65198..87acf6b1 100644 --- a/Tests/Objects/IGTestDelegateController.h +++ b/Tests/Objects/IGTestDelegateController.h @@ -13,7 +13,7 @@ @class IGTestObject; -@interface IGTestDelegateController : IGListSectionController +@interface IGTestDelegateController : IGListSectionController @property (nonatomic, strong, readonly) IGTestObject *item; diff --git a/Tests/Objects/IGTestDelegateController.m b/Tests/Objects/IGTestDelegateController.m index e79c7466..00dcdca7 100644 --- a/Tests/Objects/IGTestDelegateController.m +++ b/Tests/Objects/IGTestDelegateController.m @@ -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 *)sectionController {} +#pragma mark - IGListWorkingRangeDelegate + +- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerWillEnterWorkingRange:(IGListSectionController *)sectionController { + __unused UICollectionViewCell *cell = [self.collectionContext cellForItemAtIndex:0 sectionController:self]; +} + +- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerDidExitWorkingRange:(IGListSectionController *)sectionController {} + @end