diff --git a/CHANGELOG.md b/CHANGELOG.md index d69f3595..e93e71b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag ### Fixes +- Fixed crash when the data source is nil before calling `-[IGListAdapterUpdater performUpdateWithCollectionViewBlock:fromObjects:toObjectsBlock:animated:objectTransitionBlock:completion:]`. [Zhisheng Huang](https://github.com/lorixx) (tbd) + - Experimental fix to get the `UICollectionView` for batch updating immediately before applying the update. [Ryan Nystrom](https://github.com/rnystrom) (tbd) - `[IGListAdapterUpdater performBatchUpdatesWithCollectionViewBlock:]` and `[IGListAdapterUpdater performReloadDataWithCollectionViewBlock:]` clean state and run completion blocks if their `UICollectionView` is nil. [Brandon Darin](https://github.com/jbd1030) (tbd) diff --git a/Source/IGListAdapterUpdater.m b/Source/IGListAdapterUpdater.m index 49e69d23..778c0c5c 100644 --- a/Source/IGListAdapterUpdater.m +++ b/Source/IGListAdapterUpdater.m @@ -236,7 +236,10 @@ willPerformBatchUpdatesWithCollectionView:collectionView fromObjects:fromObjects toObjects:toObjects listIndexSetResult:result]; - if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) { + if (collectionView.dataSource == nil) { + // If the data source is nil, we should not call any collection view update. + batchUpdatesCompletionBlock(NO); + } else if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) { reloadDataFallback(); } else if (animated) { [collectionView performBatchUpdates:^{ diff --git a/Source/Internal/UICollectionView+IGListBatchUpdateData.m b/Source/Internal/UICollectionView+IGListBatchUpdateData.m index a2d44cc9..d132314c 100644 --- a/Source/Internal/UICollectionView+IGListBatchUpdateData.m +++ b/Source/Internal/UICollectionView+IGListBatchUpdateData.m @@ -11,7 +11,7 @@ @implementation UICollectionView (IGListBatchUpdateData) -- (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData { +- (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData { [self deleteItemsAtIndexPaths:updateData.deleteIndexPaths]; [self insertItemsAtIndexPaths:updateData.insertIndexPaths]; [self reloadItemsAtIndexPaths:updateData.updateIndexPaths]; diff --git a/Tests/IGListAdapterUpdaterTests.m b/Tests/IGListAdapterUpdaterTests.m index ca25660b..910a2536 100644 --- a/Tests/IGListAdapterUpdaterTests.m +++ b/Tests/IGListAdapterUpdaterTests.m @@ -97,6 +97,17 @@ XCTAssertEqual([self.collectionView numberOfSections], 0); } +- (void)test_whenReloadingDataWithNilDataSourceBefore_thatCollectionViewNotCrash { + self.dataSource.sections = @[[IGSectionObject sectionWithObjects:@[@1]], [IGSectionObject sectionWithObjects:@[@2]]]; + [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + XCTAssertEqual([self.collectionView numberOfSections], 2); + + self.collectionView.dataSource = nil; + self.dataSource.sections = @[]; + [self.updater performReloadDataWithCollectionViewBlock:[self collectionViewBlock]]; + XCTAssertEqual([self.collectionView numberOfSections], 1); // Setting collectionView's dataSource to nil would yield a single section by default. +} + - (void)test_whenInsertingSection_thatCollectionViewUpdates { NSArray *from = @[ [IGSectionObject sectionWithObjects:@[]] @@ -531,6 +542,28 @@ [mockDelegate verify]; } +- (void)test_whenCollectionViewNotInWindow_andBackgroundReloadFlag_isDefaultYES_andDataSourceWasSetToNilBefore_thatCollectionViewNotCrash { + [self.collectionView removeFromSuperview]; + + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; + self.updater.delegate = mockDelegate; + [[mockDelegate reject] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:@[] toObjects:@[] listIndexSetResult:OCMOCK_ANY]; + [[mockDelegate reject] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; + + XCTestExpectation *expectation = genExpectation; + IGListToObjectBlock to = ^NSArray *{ + return @[ + [IGSectionObject sectionWithObjects:@[]] + ]; + }; + self.collectionView.dataSource = nil; + [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:self.dataSource.sections toObjectsBlock:to animated:NO objectTransitionBlock:self.updateBlock completion:^(BOOL finished) { + [expectation fulfill]; + }]; + waitExpectation; + [mockDelegate verify]; +} + - (void)test_whenReloadBatchedWithUpdate_thatCompletionBlockStillExecuted { IGSectionObject *object = [IGSectionObject sectionWithObjects:@[@0, @1, @2]]; self.dataSource.sections = @[object]; @@ -688,6 +721,36 @@ [mockDelegate verify]; } +- (void)test_whenPerformUpdates_dataSourceWasSetToNil_shouldNotCrash { + NSArray *from = @[[IGSectionObject sectionWithObjects:@[@1] identifier:@"id1"]]; + NSArray *to = @[[IGSectionObject sectionWithObjects:@[@2] identifier:@"id1"], + [IGSectionObject sectionWithObjects:@[@22] identifier:@"id2"]]; + self.dataSource.sections = from; + id mockDelegate = [OCMockObject niceMockForProtocol:@protocol(IGListAdapterUpdaterDelegate)]; + self.updater.delegate = mockDelegate; + [mockDelegate setExpectationOrderMatters:YES]; + [[mockDelegate expect] listAdapterUpdater:self.updater willPerformBatchUpdatesWithCollectionView:self.collectionView fromObjects:from toObjects:to listIndexSetResult:[OCMArg checkWithBlock:^BOOL(IGListIndexSetResult *result) { + if (result.deletes.count != 0 || result.moves.count != 0) { + return NO; + } + // Make sure we note that index 1 is updated (id1 from @[@1] -> @[@2]), and "id2" was inserted at index 1 + return result.updates.firstIndex == 0 && result.inserts.firstIndex == 1; + }]]; + [[mockDelegate expect] listAdapterUpdater:self.updater didPerformBatchUpdates:OCMOCK_ANY collectionView:self.collectionView]; + + XCTestExpectation *expectation = genExpectation; + + // Manually set the data source to be nil. + self->_collectionView.dataSource = nil; + + [self.updater performUpdateWithCollectionViewBlock:[self collectionViewBlock] fromObjects:from toObjectsBlock:genToBlock animated:NO objectTransitionBlock:^(NSArray * _Nonnull toObjects) { + } completion:^(BOOL finished) { + [expectation fulfill]; + }]; + waitExpectation; + [mockDelegate verify]; +} + # pragma mark - preferItemReloadsFroSectionReloads - (void)test_whenReloadIsCalledWithSameItemCount_andPreferItemReload_updateIndexPathsHappen {