diff --git a/CHANGELOG.md b/CHANGELOG.md index 18ca8154..3ec6460f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ This release closes the [3.0.0 milestone](https://github.com/Instagram/IGListKit - Changed `hasChanges` methods in `IGListIndexPathResult` and `IGListIndexSetResult` to read-only properties. [Bofei Zhu](https://github.com/zhubofei) [(#453)](https://github.com/Instagram/IGListKit/pull/453) +### Enhancements + +- Empty Views now move with Refresh Controls, and no longer use the `_collectionView.backgroundView` property. [dshahidehpour](https://github.com/dshahidehpour) [(#462)](https://github.com/Instagram/IGListKit/pull/462)] + + 2.2.0 ----- diff --git a/Source/IGListAdapter.m b/Source/IGListAdapter.m index 0afdb5e1..54d5fd90 100644 --- a/Source/IGListAdapter.m +++ b/Source/IGListAdapter.m @@ -534,15 +534,22 @@ if (self.isInUpdateBlock) { return; // will be called again when update block completes } - UIView *backgroundView = [self.dataSource emptyViewForListAdapter:self]; - // don't do anything if the client is using the same view - if (backgroundView != _collectionView.backgroundView) { - // collection view will just stack the background views underneath each other if we do not remove the previous - // one first. also fine if it is nil - [_collectionView.backgroundView removeFromSuperview]; - _collectionView.backgroundView = backgroundView; + + if (shouldHide) { + _emptyListView.hidden = YES; + } else { + UIView *newBackgroundView = [self.dataSource emptyViewForListAdapter:self]; + if (_emptyListView != newBackgroundView) { + [_emptyListView removeFromSuperview]; + _emptyListView = newBackgroundView; + } + + if (_emptyListView != nil) { + _emptyListView.frame = _collectionView.bounds; + _emptyListView.hidden = NO; + [_collectionView addSubview:(id)_emptyListView]; + } } - _collectionView.backgroundView.hidden = shouldHide; } - (BOOL)itemCountIsZero { diff --git a/Source/IGListAdapterDataSource.h b/Source/IGListAdapterDataSource.h index f45ca43e..41c20cf0 100644 --- a/Source/IGListAdapterDataSource.h +++ b/Source/IGListAdapterDataSource.h @@ -56,9 +56,8 @@ NS_ASSUME_NONNULL_BEGIN @return A view to use as the collection view background, or `nil` if you don't want a background view. - @note This method is called every time the list adapter is updated. You are free to return new views every time, - but for performance reasons you may want to retain the view and return it here. The infra is only responsible for - adding the background view and maintaining its visibility. + @note This method is lazily evaluated, meaning, we won't ask for the empty view until it is necessary to display it. + You are free to return new views every time, but for performance reasons you may want to retain the view and return it here. */ - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter; diff --git a/Source/Internal/IGListAdapterInternal.h b/Source/Internal/IGListAdapterInternal.h index 73d8cb97..9406fad4 100644 --- a/Source/Internal/IGListAdapterInternal.h +++ b/Source/Internal/IGListAdapterInternal.h @@ -40,7 +40,7 @@ IGListCollectionContext @property (nonatomic, strong, nullable) IGListAdapterProxy *delegateProxy; -@property (nonatomic, strong, nullable) UIView *emptyBackgroundView; +@property (nonatomic, strong, nullable) UIView *emptyListView; /** When making object updates inside a batch update block, delete operations must use the section /before/ any moves take diff --git a/Tests/IGListAdapterTests.m b/Tests/IGListAdapterTests.m index 403ef24f..5e79ad25 100644 --- a/Tests/IGListAdapterTests.m +++ b/Tests/IGListAdapterTests.m @@ -181,19 +181,40 @@ XCTAssertEqual(CGPointEqualToPoint(point, p), YES); \ XCTAssertEqualObjects(identifier, @"IGNibNameUICollectionViewCell"); } -- (void)test_whenDataSourceChanges_thatBackgroundViewVisibilityChanges { +- (void)test_EmptyViewIsCreatedLazily { + UIView *emptyView = [[UIView alloc] init]; + self.dataSource.emptyView = emptyView; self.dataSource.objects = @[@1]; - UIView *background = [[UIView alloc] init]; - self.dataSource.backgroundView = background; __block BOOL executed = NO; [self.adapter reloadDataWithCompletion:^(BOOL finished) { - XCTAssertTrue(self.adapter.collectionView.backgroundView.hidden, @"Background view should be hidden"); - XCTAssertEqualObjects(background, self.adapter.collectionView.backgroundView, @"Background view not correctly assigned"); + XCTAssertNil(self.adapter.emptyListView, @"Empty list view not correctly assigned"); self.dataSource.objects = @[]; [self.adapter reloadDataWithCompletion:^(BOOL finished2) { - XCTAssertFalse(self.adapter.collectionView.backgroundView.hidden, @"Background view should be visible"); - XCTAssertEqualObjects(background, self.adapter.collectionView.backgroundView, @"Background view not correctly assigned"); + XCTAssertFalse(self.adapter.emptyListView.hidden, @"Empty list view should be visible"); + XCTAssertEqualObjects(emptyView, self.adapter.emptyListView, @"Empty list view not correctly assigned"); + executed = YES; + }]; + }]; + XCTAssertTrue(executed); +} + +- (void)test_whenDataSourceChanges_thatEmptyViewVisibilityChanges { + UIView *emptyView = [[UIView alloc] init]; + self.dataSource.emptyView = emptyView; + self.dataSource.objects = @[]; + [self.adapter reloadDataWithCompletion:nil]; + + self.dataSource.objects = @[@1]; + __block BOOL executed = NO; + [self.adapter reloadDataWithCompletion:^(BOOL finished) { + XCTAssertTrue(self.adapter.emptyListView.hidden, @"Empty list view should be hidden"); + XCTAssertEqualObjects(emptyView, self.adapter.emptyListView, @"Empty list view not correctly assigned"); + + self.dataSource.objects = @[]; + [self.adapter reloadDataWithCompletion:^(BOOL finished2) { + XCTAssertFalse(self.adapter.emptyListView.hidden, @"Empty list view should be visible"); + XCTAssertEqualObjects(emptyView, self.adapter.emptyListView, @"Empty list view not correctly assigned"); executed = YES; }]; }]; @@ -315,54 +336,54 @@ XCTAssertEqual(CGPointEqualToPoint(point, p), YES); \ - (void)test_whenDataSourceAddsItems_thatEmptyViewBecomesVisible { self.dataSource.objects = @[]; - UIView *background = [UIView new]; - self.dataSource.backgroundView = background; + UIView *emptyView = [UIView new]; + self.dataSource.emptyView = emptyView; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertEqual(self.collectionView.backgroundView, background); - XCTAssertFalse(self.collectionView.backgroundView.hidden); + XCTAssertEqual(self.adapter.emptyListView, emptyView); + XCTAssertFalse(self.adapter.emptyListView.hidden); self.dataSource.objects = @[@2]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + XCTAssertTrue(self.adapter.emptyListView.hidden); } - (void)test_whenInsertingIntoEmptySection_thatEmptyViewBecomesHidden { self.dataSource.objects = @[@0]; - self.dataSource.backgroundView = [UIView new]; + self.dataSource.emptyView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertFalse(self.collectionView.backgroundView.hidden); + XCTAssertFalse(self.adapter.emptyListView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(0)]; sectionController.items = 1; [self.adapter insertInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + XCTAssertTrue(self.adapter.emptyListView.hidden); } - (void)test_whenDeletingAllItemsFromSection_thatEmptyViewBecomesVisible { self.dataSource.objects = @[@1]; - self.dataSource.backgroundView = [UIView new]; + self.dataSource.emptyView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + XCTAssertNil(self.adapter.emptyListView); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(1)]; sectionController.items = 0; [self.adapter deleteInSectionController:sectionController atIndexes:[NSIndexSet indexSetWithIndex:0]]; - XCTAssertFalse(self.collectionView.backgroundView.hidden); + XCTAssertFalse(self.adapter.emptyListView.hidden); } - (void)test_whenEmptySectionAddsItems_thatEmptyViewBecomesHidden { self.dataSource.objects = @[@0]; - self.dataSource.backgroundView = [UIView new]; + self.dataSource.emptyView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertFalse(self.collectionView.backgroundView.hidden); + XCTAssertFalse(self.adapter.emptyListView.hidden); IGListTestSection *sectionController = [self.adapter sectionControllerForObject:@(0)]; sectionController.items = 2; [self.adapter reloadSectionController:sectionController]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + XCTAssertTrue(self.adapter.emptyListView.hidden); } - (void)test_whenSectionItemsAreDeletedAsBatch_thatEmptyViewBecomesVisible { self.dataSource.objects = @[@1, @2]; - self.dataSource.backgroundView = [UIView new]; + self.dataSource.emptyView = [UIView new]; [self.adapter reloadDataWithCompletion:nil]; - XCTAssertTrue(self.collectionView.backgroundView.hidden); + XCTAssertNil(self.adapter.emptyListView); IGListTestSection *firstSectionController = [self.adapter sectionControllerForObject:@(1)]; IGListTestSection *secondSectionController = [self.adapter sectionControllerForObject:@(2)]; XCTestExpectation *expectation = [self expectationWithDescription:NSStringFromSelector(_cmd)]; @@ -373,7 +394,7 @@ XCTAssertEqual(CGPointEqualToPoint(point, p), YES); \ NSIndexSet *indexesToDelete = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]; [self.adapter deleteInSectionController:secondSectionController atIndexes:indexesToDelete]; } completion:^(BOOL finished) { - XCTAssertFalse(self.collectionView.backgroundView.hidden); + XCTAssertFalse(self.adapter.emptyListView.hidden); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:15 handler:nil]; diff --git a/Tests/Objects/IGListTestAdapterDataSource.h b/Tests/Objects/IGListTestAdapterDataSource.h index e873e975..0907c829 100644 --- a/Tests/Objects/IGListTestAdapterDataSource.h +++ b/Tests/Objects/IGListTestAdapterDataSource.h @@ -16,6 +16,6 @@ // array of numbers which is then passed to -[IGListTestSection setItems:] @property (nonatomic, strong) NSArray *objects; -@property (nonatomic, strong) UIView *backgroundView; +@property (nonatomic, strong) UIView *emptyView; @end diff --git a/Tests/Objects/IGListTestAdapterDataSource.m b/Tests/Objects/IGListTestAdapterDataSource.m index 19048871..cc574b50 100644 --- a/Tests/Objects/IGListTestAdapterDataSource.m +++ b/Tests/Objects/IGListTestAdapterDataSource.m @@ -25,7 +25,7 @@ } - (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter { - return self.backgroundView; + return self.emptyView; } @end