Skip to content

Commit

Permalink
Prevent data source deallocation during async updates
Browse files Browse the repository at this point in the history
Summary: Saw some crashes come in from out-of-sync objects and section controllers. After some digging I found that the nil-datasource assert was firing (not very frequently). Mostly happening on other VCs, probably from hitting the back button and an update arriving.

Reviewed By: jessesquires

Differential Revision: D4347862

fbshipit-source-id: 38c1a4816f5c109de41297309745ac2d5e348e93
  • Loading branch information
Ryan Nystrom authored and Facebook Github Bot committed Dec 19, 2016
1 parent 57c9f84 commit 4cc91a2
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ This release closes the [2.1.0 milestone](https://github.com/Instagram/IGListKit

- Avoid `UICollectionView` crashes when queueing a reload and insert/delete on the same item as well as reloading an item in a section that is animating. [Ryan Nystrom](https://github.com/rnystrom) [(#325)](https://github.com/Instagram/IGListKit/pull/325)

- Prevent adapter data source from deallocating after queueing an update. [Ryan Nystrom](https://github.com/rnystrom) (tbd)

2.0.0
-----

Expand Down
17 changes: 10 additions & 7 deletions Source/IGListAdapter.m
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ - (void)setScrollViewDelegate:(id<UIScrollViewDelegate>)scrollViewDelegate {
}

- (void)updateAfterPublicSettingsChange {
if (_collectionView != nil && _dataSource != nil) {
[self updateObjects:[[_dataSource objectsForListAdapter:self] copy]];
id<IGListAdapterDataSource> dataSource = _dataSource;
if (_collectionView != nil && dataSource != nil) {
[self updateObjects:[[dataSource objectsForListAdapter:self] copy] dataSource:dataSource];
}
}

Expand Down Expand Up @@ -267,7 +268,7 @@ - (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletio
// there are any item deletes at the same
weakSelf.previoussectionMap = [weakSelf.sectionMap copy];

[weakSelf updateObjects:toObjects];
[weakSelf updateObjects:toObjects dataSource:dataSource];
} completion:^(BOOL finished) {
// release the previous items
weakSelf.previoussectionMap = nil;
Expand Down Expand Up @@ -296,7 +297,7 @@ - (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion {
[self.updatingDelegate reloadDataWithCollectionView:collectionView reloadUpdateBlock:^{
// purge all section controllers from the item map so that they are regenerated
[weakSelf.sectionMap reset];
[weakSelf updateObjects:newItems];
[weakSelf updateObjects:newItems dataSource:dataSource];
} completion:completion];
}

Expand Down Expand Up @@ -437,7 +438,9 @@ - (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NS

// this method is what updates the "source of truth"
// this should only be called just before the collection view is updated
- (void)updateObjects:(NSArray *)objects {
- (void)updateObjects:(NSArray *)objects dataSource:(id<IGListAdapterDataSource>)dataSource {
IGParameterAssert(dataSource != nil);

#if DEBUG
for (id object in objects) {
IGAssert([object isEqual:object], @"Object instance %@ not equal to itself. This will break infra map tables.", object);
Expand All @@ -463,10 +466,10 @@ - (void)updateObjects:(NSArray *)objects {

// if not, query the data source for a new one
if (sectionController == nil) {
sectionController = [self.dataSource listAdapter:self sectionControllerForObject:object];
sectionController = [dataSource listAdapter:self sectionControllerForObject:object];
}

IGAssert(sectionController != nil, @"Data source <%@> cannot return a nil section controller.", self.dataSource);
IGAssert(sectionController != nil, @"Data source <%@> cannot return a nil section controller.", dataSource);
if (sectionController == nil) {
continue;
}
Expand Down
23 changes: 23 additions & 0 deletions Tests/IGListAdapterE2ETests.m
Original file line number Diff line number Diff line change
Expand Up @@ -1136,4 +1136,27 @@ - (void)test_whenReloadingItems_withSectionDeletedInFront_thatUpdateCanBeApplied
[self waitForExpectationsWithTimeout:15 handler:nil];
}

- (void)test_whenDataSourceDeallocatedAfterUpdateQueued_thatUpdateSuccesfullyCompletes {
IGTestDelegateDataSource *dataSource = [IGTestDelegateDataSource new];
dataSource.objects = @[genTestObject(@1, @1)];
self.adapter.collectionView = self.collectionView;
self.adapter.dataSource = dataSource;
[self.collectionView layoutIfNeeded];

dataSource.objects = @[
genTestObject(@1, @1),
genTestObject(@2, @2),
];

XCTestExpectation *expectation = genExpectation;
[self.adapter performUpdatesAnimated:YES completion:^(BOOL finished) {
XCTAssertEqual([self.collectionView numberOfSections], 2);
[expectation fulfill];
}];

dataSource = nil;

[self waitForExpectationsWithTimeout:15 handler:nil];
}

@end

0 comments on commit 4cc91a2

Please sign in to comment.