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
This commit is contained in:
Dustin Shahidehpour 2017-02-06 08:07:30 -08:00 committed by Facebook Github Bot
parent 8a645139b2
commit fce3286ae4
7 changed files with 70 additions and 38 deletions

View file

@ -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
-----

View file

@ -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 {

View file

@ -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;

View file

@ -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

View file

@ -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];

View file

@ -16,6 +16,6 @@
// array of numbers which is then passed to -[IGListTestSection setItems:]
@property (nonatomic, strong) NSArray <NSNumber *> *objects;
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UIView *emptyView;
@end

View file

@ -25,7 +25,7 @@
}
- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter {
return self.backgroundView;
return self.emptyView;
}
@end