From fce3286ae421f339a983d90b6f90e4d565ed2c56 Mon Sep 17 00:00:00 2001 From: Dustin Shahidehpour Date: Mon, 6 Feb 2017 08:07:30 -0800 Subject: [PATCH] Decouple Empty View from backgroundView, make it move with PTR Summary: 1. The "Empty View" and CV background view are different things. People can now differentiate. 2. The Empty View isn't created until it is needed. 3. The Empty View moves with Refresh Controls. ![](https://media.giphy.com/media/26gslZ7qP07e4N9h6/giphy.gif) - [x] All tests pass. Demo project builds and runs. - [x] I added tests, an experiment, or detailed why my change isn't tested. - [x] I added an entry to the `CHANGELOG.md` for any breaking changes, enhancements, or bug fixes. - [x] I have reviewed the [contributing guide](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) Closes https://github.com/Instagram/IGListKit/pull/462 Reviewed By: rnystrom Differential Revision: D4502591 Pulled By: dshahidehpour fbshipit-source-id: b72b444c1197c90c385c7414f0662299070a86d1 --- CHANGELOG.md | 5 ++ Source/IGListAdapter.m | 23 ++++--- Source/IGListAdapterDataSource.h | 5 +- Source/Internal/IGListAdapterInternal.h | 2 +- Tests/IGListAdapterTests.m | 69 ++++++++++++++------- Tests/Objects/IGListTestAdapterDataSource.h | 2 +- Tests/Objects/IGListTestAdapterDataSource.m | 2 +- 7 files changed, 70 insertions(+), 38 deletions(-) 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