How to make Supplementary View float in UICollectionView as Section Headers do in UITableView plain style

后端 未结 13 617
逝去的感伤
逝去的感伤 2020-11-27 09:35

I\'m struggling to achieve a \"floating section header\" effect with UICollectionView. Something that\'s been easy enough in UITableView (default b

相关标签:
13条回答
  • 2020-11-27 10:11

    There is a bug in cocotouch's post. When there is no items in section and the section footer were set to be not displayed, the section header will go outside of the collection view and the user will be not able to see it.

    In fact change:

    if (numberOfItemsInSection > 0) {
        firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
        lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
    } else {
        firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                atIndexPath:firstObjectIndexPath];
        lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                               atIndexPath:lastObjectIndexPath];
    }
    

    into:

    if (numberOfItemsInSection > 0) {
        firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
        lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
    } else {
        firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                atIndexPath:firstObjectIndexPath];
        lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                               atIndexPath:lastObjectIndexPath];
        if (lastObjectAttrs == nil) {
            lastObjectAttrs = firstObjectAttrs;
        }
    }
    

    will solve this issue.

    0 讨论(0)
  • 2020-11-27 10:19

    In iOS9, Apple was kind enough to add a simple property in UICollectionViewFlowLayout called sectionHeadersPinToVisibleBounds.

    With this, you can make the headers float like that in table views.

    let layout = UICollectionViewFlowLayout()
    layout.sectionHeadersPinToVisibleBounds = true
    layout.minimumInteritemSpacing = 1
    layout.minimumLineSpacing = 1
    super.init(collectionViewLayout: layout)
    
    0 讨论(0)
  • 2020-11-27 10:22

    Either implement the following delegate methods:

    – collectionView:layout:sizeForItemAtIndexPath:
    – collectionView:layout:insetForSectionAtIndex:
    – collectionView:layout:minimumLineSpacingForSectionAtIndex:
    – collectionView:layout:minimumInteritemSpacingForSectionAtIndex:
    – collectionView:layout:referenceSizeForHeaderInSection:
    – collectionView:layout:referenceSizeForFooterInSection:
    

    In your view controller that has your :cellForItemAtIndexPath method (just return the correct values). Or, instead of using the delegate methods, you may also set these values directly in your layout object, e.g. [layout setItemSize:size];.

    Using either of these methods will enable you to set your settings in Code rather than IB as they're removed when you set a Custom Layout. Remember to add <UICollectionViewDelegateFlowLayout> to your .h file, too!

    Create a new Subclass of UICollectionViewFlowLayout, call it whatever you want, and make sure the H file has:

    #import <UIKit/UIKit.h>
    
    @interface YourSubclassNameHere : UICollectionViewFlowLayout
    
    @end
    

    Inside the Implementation File make sure it has the following:

    - (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {
    
        NSMutableArray *answer = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
        UICollectionView * const cv = self.collectionView;
        CGPoint const contentOffset = cv.contentOffset;
    
        NSMutableIndexSet *missingSections = [NSMutableIndexSet indexSet];
        for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
            if (layoutAttributes.representedElementCategory == UICollectionElementCategoryCell) {
                [missingSections addIndex:layoutAttributes.indexPath.section];
            }
        }
        for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
            if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
                [missingSections removeIndex:layoutAttributes.indexPath.section];
            }
        }
    
        [missingSections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
    
            NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:idx];
    
            UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath];
    
            [answer addObject:layoutAttributes];
    
        }];
    
        for (UICollectionViewLayoutAttributes *layoutAttributes in answer) {
    
            if ([layoutAttributes.representedElementKind isEqualToString:UICollectionElementKindSectionHeader]) {
    
                NSInteger section = layoutAttributes.indexPath.section;
                NSInteger numberOfItemsInSection = [cv numberOfItemsInSection:section];
    
                NSIndexPath *firstCellIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
                NSIndexPath *lastCellIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];
    
                NSIndexPath *firstObjectIndexPath = [NSIndexPath indexPathForItem:0 inSection:section];
                NSIndexPath *lastObjectIndexPath = [NSIndexPath indexPathForItem:MAX(0, (numberOfItemsInSection - 1)) inSection:section];
    
                UICollectionViewLayoutAttributes *firstObjectAttrs;
                UICollectionViewLayoutAttributes *lastObjectAttrs;
    
                if (numberOfItemsInSection > 0) {
                    firstObjectAttrs = [self layoutAttributesForItemAtIndexPath:firstObjectIndexPath];
                    lastObjectAttrs = [self layoutAttributesForItemAtIndexPath:lastObjectIndexPath];
                } else {
                    firstObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader
                                                                        atIndexPath:firstObjectIndexPath];
                    lastObjectAttrs = [self layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionFooter
                                                                       atIndexPath:lastObjectIndexPath];
                }
    
                CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
                CGPoint origin = layoutAttributes.frame.origin;
                origin.y = MIN(
                               MAX(
                                   contentOffset.y + cv.contentInset.top,
                                   (CGRectGetMinY(firstObjectAttrs.frame) - headerHeight)
                                   ),
                               (CGRectGetMaxY(lastObjectAttrs.frame) - headerHeight)
                               );
    
                layoutAttributes.zIndex = 1024;
                layoutAttributes.frame = (CGRect){
                    .origin = origin,
                    .size = layoutAttributes.frame.size
                };
    
            }
    
        }
    
        return answer;
    
    }
    
    - (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBound {
    
        return YES;
    
    }
    

    Choose "Custom" in Interface Builder for the Flow Layout, choose your "YourSubclassNameHere" Class that you just created. And Run!

    (Note: the code above may not respect contentInset.bottom values, or especially large or small footer objects, or collections that have 0 objects but no footer.)

    0 讨论(0)
  • 2020-11-27 10:22

    I've added a sample on github that is pretty simple, I think.

    Basically the strategy is to provide a custom layout that invalidates on bounds change and provide layout attributes for the supplementary view that hug the current bounds. As others have suggested. I hope the code is useful.

    0 讨论(0)
  • 2020-11-27 10:22

    VCollectionViewGridLayout does sticky headers. It is a vertical scrolling simple grid layout based on TLIndexPathTools. Try running the Sticky Headers sample project.

    This layout also has much better batch update animation behavior than UICollectionViewFlowLayout. There are a couple of sample projects provided that let you toggle between the two layouts to demonstrate the improvement.

    0 讨论(0)
  • 2020-11-27 10:26

    Here is my take on it, I think it's a lot simpler than what a glimpsed above. The main source of simplicity is that I'm not subclassing flow layout, rather rolling my own layout (much easier, if you ask me).

    Please Note I am assuming you are already capable of implementing your own custom UICollectionViewLayout that will display cells and headers without floating implemented. Once you have that implementation written, only then will the code below make any sense. Again, this is because the OP was asking specifically about the floating headers part.

    a few bonuses:

    1. I am floating two headers, not just one
    2. Headers push previous headers out of the way
    3. Look, swift!

    note:

    1. supplementaryLayoutAttributes contains all header attributes without floating implemented
    2. I am using this code in prepareLayout, since I do all computation upfront.
    3. don't forget to override shouldInvalidateLayoutForBoundsChange to true!

    // float them headers
    let yOffset = self.collectionView!.bounds.minY
    let headersRect = CGRect(x: 0, y: yOffset, width: width, height: headersHeight)
    
    var floatingAttributes = supplementaryLayoutAttributes.filter {
        $0.frame.minY < headersRect.maxY
    }
    
    // This is three, because I am floating 2 headers
    // so 2 + 1 extra that will be pushed away
    var index = 3
    var floatingPoint = yOffset + dateHeaderHeight
    
    while index-- > 0 && !floatingAttributes.isEmpty {
    
        let attribute = floatingAttributes.removeLast()
        attribute.frame.origin.y = max(floatingPoint, attribute.frame.origin.y)
    
        floatingPoint = attribute.frame.minY - dateHeaderHeight
    }
    
    0 讨论(0)
提交回复
热议问题