From e49c94b25d51f5397fa7608f2c067fbbaa974204 Mon Sep 17 00:00:00 2001 From: Marcus Wu Date: Mon, 9 Apr 2018 08:28:05 -0700 Subject: [PATCH] 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 --- CHANGELOG.md | 2 + Source/IGListCollectionViewLayout.h | 5 ++ Source/IGListCollectionViewLayout.mm | 55 ++++++++++--------- Tests/IGListCollectionViewLayoutTests.m | 70 +++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d67cbd..26cad7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Source/IGListCollectionViewLayout.h b/Source/IGListCollectionViewLayout.h index 906e5641..c3e9b897 100644 --- a/Source/IGListCollectionViewLayout.h +++ b/Source/IGListCollectionViewLayout.h @@ -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. diff --git a/Source/IGListCollectionViewLayout.mm b/Source/IGListCollectionViewLayout.mm index a8af8eca..fd50d821 100644 --- a/Source/IGListCollectionViewLayout.mm +++ b/Source/IGListCollectionViewLayout.mm @@ -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(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 diff --git a/Tests/IGListCollectionViewLayoutTests.m b/Tests/IGListCollectionViewLayoutTests.m index 035d7cd0..2307dc50 100644 --- a/Tests/IGListCollectionViewLayoutTests.m +++ b/Tests/IGListCollectionViewLayoutTests.m @@ -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];