Show header when section item is empty

Summary:
Issue fixed: #1117

I adding a new constructor for making a `IGListCollectionViewLayout` instance that can always show sticky header although section data is empty.

It's working well and [this is demo project](https://github.com/marcuswu0814/IGListKit_ShowStickyHeaderWhenDataEmpty).

Bug I'm not sure is any good way to let this much more readability. Is there any good advice?

```objc
const CGRect headerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ?
                CGRectMake(insets.left,
                        (itemCount == 0) ? CGRectGetMaxY(rollingSectionBounds) : CGRectGetMinY(rollingSectionBounds) - headerSize.height,
                        paddedLengthInFixedDirection,
                        headerSize.height) :
                CGRectMake((itemCount == 0) ? CGRectGetMaxX(rollingSectionBounds) : CGRectGetMinX(rollingSectionBounds) - headerSize.width,
                        insets.top,
                        headerSize.width,
                        paddedLengthInFixedDirection);
```

- [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/1129

Differential Revision: D7551628

Pulled By: rnystrom

fbshipit-source-id: a60b65a92efcea5175c86aaed1de02686ea6d20a
This commit is contained in:
Marcus Wu 2018-04-09 08:28:05 -07:00 committed by Facebook Github Bot
parent 9315299fbc
commit e49c94b25d
4 changed files with 107 additions and 25 deletions

View file

@ -11,6 +11,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag
- 5x improvement to diffing performance when result is only inserts or deletes. [Ryan Nystrom](https://github.com/rnystrom) [(tbd)](tbd)
- Can always show sticky header although section data is empty. [Marcus Wu](https://github.com/marcuswu0814) [(#1129)](https://github.com/Instagram/IGListKit/pull/1129)
- Added `-[IGListCollectionContext dequeueReusableCellOfClass:withReuseIdentifier:forSectionController:atIndex:]` to allow for registering cells of the same class with different reuse identifiers. [Jeremy Lawrence](https://github.com/Ziewvater) (tbd)
### Fixes

View file

@ -88,6 +88,11 @@ NS_SWIFT_NAME(ListCollectionViewLayout)
*/
@property (nonatomic, assign) CGFloat stickyHeaderYOffset;
/**
Set this to `YES` to show sticky header when a section had no item. Default is `NO`.
*/
@property (nonatomic, assign) BOOL showHeaderWhenEmpty;
/**
Notify the layout that a specific section was modified before invalidation. Used to optimize layout re-calculation.

View file

@ -245,7 +245,7 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
const NSInteger itemCount = _sectionData[section].itemBounds.size();
// do not add headers if there are no items
if (itemCount > 0) {
if (itemCount > 0 || self.showHeaderWhenEmpty) {
for (NSString *elementKind in _supplementaryAttributesCache.allKeys) {
NSIndexPath *indexPath = indexPathForSection(section);
UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:elementKind
@ -319,7 +319,6 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) {
frame = entry.headerBounds;
if (self.stickyHeaders) {
CGFloat offset = CGPointGetCoordinateInDirection(collectionView.contentOffset, self.scrollDirection) + self.topContentInset + self.stickyHeaderYOffset;
@ -484,6 +483,8 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
for (NSInteger section = _minimumInvalidatedSection; section < sectionCount; section++) {
const NSInteger itemCount = [dataSource collectionView:collectionView numberOfItemsInSection:section];
const BOOL itemsEmpty = itemCount == 0;
const BOOL hideHeaderWhenItemsEmpty = itemsEmpty && !self.showHeaderWhenEmpty;
_sectionData[section].itemBounds = std::vector<CGRect>(itemCount);
const CGSize headerSize = [delegate collectionView:collectionView layout:self referenceSizeForHeaderInSection:section];
@ -495,8 +496,8 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
const CGSize paddedCollectionViewSize = UIEdgeInsetsInsetRect(contentInsetAdjustedCollectionViewBounds, insets).size;
const UICollectionViewScrollDirection fixedDirection = self.scrollDirection == UICollectionViewScrollDirectionHorizontal ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal;
const CGFloat paddedLengthInFixedDirection = CGSizeGetLengthInDirection(paddedCollectionViewSize, fixedDirection);
const CGFloat headerLengthInScrollDirection = CGSizeGetLengthInDirection(headerSize, self.scrollDirection);
const CGFloat footerLengthInScrollDirection = CGSizeGetLengthInDirection(footerSize, self.scrollDirection);
const CGFloat headerLengthInScrollDirection = hideHeaderWhenItemsEmpty ? 0 : CGSizeGetLengthInDirection(headerSize, self.scrollDirection);
const CGFloat footerLengthInScrollDirection = hideHeaderWhenItemsEmpty ? 0 : CGSizeGetLengthInDirection(footerSize, self.scrollDirection);
const BOOL headerExists = headerLengthInScrollDirection > 0;
const BOOL footerExists = footerLengthInScrollDirection > 0;
@ -577,29 +578,33 @@ static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attribut
rollingSectionBounds = CGRectUnion(rollingSectionBounds, frame);
}
}
const CGRect headerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ?
CGRectMake(insets.left,
CGRectGetMinY(rollingSectionBounds) - headerSize.height,
paddedLengthInFixedDirection,
headerSize.height) :
CGRectMake(CGRectGetMinX(rollingSectionBounds) - headerSize.width,
insets.top,
headerSize.width,
paddedLengthInFixedDirection);
const CGRect headerBounds = self.scrollDirection == UICollectionViewScrollDirectionVertical ?
CGRectMake(insets.left,
itemsEmpty ? CGRectGetMaxY(rollingSectionBounds) : CGRectGetMinY(rollingSectionBounds) - headerSize.height,
paddedLengthInFixedDirection,
hideHeaderWhenItemsEmpty ? 0 : headerSize.height) :
CGRectMake(itemsEmpty ? CGRectGetMaxX(rollingSectionBounds) : CGRectGetMinX(rollingSectionBounds) - headerSize.width,
insets.top,
hideHeaderWhenItemsEmpty ? 0 : headerSize.width,
paddedLengthInFixedDirection);
_sectionData[section].headerBounds = headerBounds;
if (itemsEmpty) {
rollingSectionBounds = headerBounds;
}
const CGRect footerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ?
CGRectMake(insets.left,
CGRectGetMaxY(rollingSectionBounds),
paddedLengthInFixedDirection,
footerSize.height) :
CGRectMake(CGRectGetMaxX(rollingSectionBounds) + insets.right,
insets.top,
footerSize.width,
paddedLengthInFixedDirection);
CGRectMake(insets.left,
CGRectGetMaxY(rollingSectionBounds),
paddedLengthInFixedDirection,
hideHeaderWhenItemsEmpty ? 0 : footerSize.height) :
CGRectMake(CGRectGetMaxX(rollingSectionBounds) + insets.right,
insets.top,
hideHeaderWhenItemsEmpty ? 0 : footerSize.width,
paddedLengthInFixedDirection);
_sectionData[section].footerBounds = footerBounds;
// union the header before setting the bounds of the section

View file

@ -41,6 +41,12 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
return [self.collectionView supplementaryViewForElementKind:UICollectionElementKindSectionFooter atIndexPath:genIndexPath(section, 0)];
}
- (void)setUpWithStickyHeaders:(BOOL)sticky showHeaderWhenEmpty:(BOOL)showHeaderWhenEmpty {
self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:YES topContentInset:0 stretchToEdge:NO];
self.layout.showHeaderWhenEmpty = showHeaderWhenEmpty;
[self setUpCollectionViewAndDataSource:kTestFrame];
}
- (void)setUpWithStickyHeaders:(BOOL)sticky topInset:(CGFloat)inset {
[self setUpWithStickyHeaders:sticky topInset:inset stretchToEdge:NO];
}
@ -55,6 +61,10 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
- (void)setUpWithStickyHeaders:(BOOL)sticky scrollDirection:(UICollectionViewScrollDirection)scrollDirection topInset:(CGFloat)inset stretchToEdge:(BOOL)stretchToEdge testFrame:(CGRect)testFrame {
self.layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:sticky scrollDirection:scrollDirection topContentInset:inset stretchToEdge:stretchToEdge];
[self setUpCollectionViewAndDataSource:testFrame];
}
- (void)setUpCollectionViewAndDataSource:(CGRect)testFrame {
self.dataSource = [IGLayoutTestDataSource new];
self.collectionView = [[UICollectionView alloc] initWithFrame:testFrame collectionViewLayout:self.layout];
self.collectionView.dataSource = self.dataSource;
@ -86,6 +96,66 @@ static const CGRect kTestFrame = (CGRect){{0, 0}, {100, 100}};
XCTAssertTrue(CGSizeEqualToSize(CGSizeZero, self.collectionView.contentSize));
}
- (void)test_whenSectionDataIsEmpty_thatStickyHeaderStillShow {
[self setUpWithStickyHeaders:YES showHeaderWhenEmpty:YES];
[self prepareWithData:@[[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:10
footerHeight:0
items:nil],
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:20
footerHeight:0
items:nil],
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:30
footerHeight:0
items:nil]]];
IGAssertEqualFrame([self headerForSection:0].frame, 0, 0, 100, 10);
IGAssertEqualFrame([self headerForSection:1].frame, 0, 10, 100, 20);
IGAssertEqualFrame([self headerForSection:2].frame, 0, 30, 100, 30);
}
- (void)test_whenSectionDataIsEmpty_thatStickyHeaderShouldBeHidden {
[self setUpWithStickyHeaders:YES showHeaderWhenEmpty:NO];
[self prepareWithData:@[[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:10
footerHeight:0
items:@[
[[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}]
]],
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:20
footerHeight:0
items:nil],
[[IGLayoutTestSection alloc] initWithInsets:UIEdgeInsetsZero
lineSpacing:0
interitemSpacing:0
headerHeight:20
footerHeight:0
items:@[
[[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 10}],
[[IGLayoutTestItem alloc] initWithSize:(CGSize) {85, 20}],
]]
]];
IGAssertEqualFrame([self headerForSection:0].frame, 0, 0, 100, 10);
IGAssertEqualFrame([self headerForSection:1].frame, 0, 0, 0, 0);
IGAssertEqualFrame([self headerForSection:2].frame, 0, 20, 100, 20);
}
- (void)test_whenLayingOutCellsVertically_withHeaderHeight_withLineSpacing_withInsets_thatFramesCorrect {
[self setUpWithStickyHeaders:NO topInset:0];