By 100grams


2012-11-22 11:25:11 8 Comments

I'm struggling to achieve a "floating section header" effect with UICollectionView. Something that's been easy enough in UITableView (default behavior for UITableViewStylePlain) seems impossible in UICollectionView without lots of hard work. Am I missing the obvious?

Apple provides no documentation on how to achieve this. It seems that one has to subclass UICollectionViewLayout and implement a custom layout just to achieve this effect. This entails quite a bit of work, implementing the following methods:

Methods to Override

Every layout object should implement the following methods:

collectionViewContentSize
layoutAttributesForElementsInRect:
layoutAttributesForItemAtIndexPath:
layoutAttributesForSupplementaryViewOfKind:atIndexPath: (if your layout supports supplementary views)
layoutAttributesForDecorationViewOfKind:atIndexPath: (if your layout supports decoration views)
shouldInvalidateLayoutForBoundsChange:

However its not clear to me how to make the supplementary view float above the cells and "stick" to the top of the view until the next section is reached. Is there a flag for this in the layout attributes?

I would have used UITableView but I need to create a rather complex hierarchy of collections which is easily achieved with a collection view.

Any guidance or sample code would be greatly appreciated!

12 comments

@Constantino Tsarouhas 2018-11-24 13:39:09

@iPrabu had an excellent answer with sectionHeadersPinToVisibleBounds. I’ll just add that you can set this property in Interface Builder as well:

  1. Select the flow layout object in the document navigator. (If collapsed, expand it first using the toolbar button in the lower-left corner of the editor.)

Selecting the flow layout object in the document navigator.

  1. Open the Identity inspector and add a user-defined runtime attribute with key path sectionHeadersPinToVisibleBounds, type Boolean, and the checkbox checked.

Setting the runtime attribute in the Identity inspector.

The default header view has a transparent background. You might want to make it (partially) opaque or add a blur effect view.

@iPrabu 2015-11-27 15:55:20

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)

@Happiehappie 2016-02-11 12:22:13

Where do I add this snippet

@iPrabu 2016-02-11 12:27:55

In your UICollectionViewController init method

@Happiehappie 2016-02-11 12:29:25

What if I am using an UIViewController with a collectionView in it

@iPrabu 2016-02-11 12:36:24

You can set the value like yourCollectionView.collectionViewLayout. sectionHeadersPinToVisibleBounds = true

@Josip B. 2016-03-17 08:35:42

Great answer! I'm guessing that answers above are only solution for pre iOS 9?

@JohnnyAW 2016-05-07 08:57:27

just a little correction: to use yourCollectionView.collectionViewLayout you need to cast the layout to UICollectionViewFlowLayout: (self.yourCollectionView.collectionViewLayout as! UICollectionViewFlowLayout).sectionHeadersPinToVisibleBounds = true

@GeneCode 2017-05-04 03:31:43

In case anybody is looking for a solution in Objective-C, put this in viewDidload:

    UICollectionViewFlowLayout *flowLayout = 
    (UICollectionViewFlowLayout*)_yourcollectionView.collectionViewLayout;
    [flowLayout setSectionHeadersPinToVisibleBounds:YES];

@Zaid Pathan 2016-06-07 06:50:56

If already set flow layout in Storyboard or Xib file then try this,

(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionHeadersPinToVisibleBounds = true

@Jignesh Chanchiya 2019-04-11 09:31:53

Great, work for me, you save my day. Thanks

@Mazyod 2014-11-26 21:31:03

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
}

@SafeFastExpressive 2015-05-03 15:37:01

Not sure what this means, the code is incomplete and the notes don't fill in all the gaps.

@Mazyod 2015-05-03 16:40:41

@RandyHill This code isn't meant to be a generic solution, rather it's a guide towards how I achieved floating headers. If you have trouble understanding any part of the implementation, just ask. (I'll add an extra note, which I think is the source of your confusion).

@SafeFastExpressive 2015-05-03 23:59:26

Never mind, this is clear in relation to the question. I've removed my down vote.

@gasparuff 2015-10-15 08:45:44

Do you have any example project maybe? I'm trying to implement 2 floating headers and can't make it work :-(

@WeZZard 2013-09-08 04:49:32

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.

@user4047976 2016-06-30 12:27:47

Thank You Bro.... @WeZZard

@Timothy Moose 2013-07-11 04:21:03

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.

@xaphod 2014-08-28 13:58:53

I tried that. Actually if you already have a UICollectionView with a working data delegate, then moving to this is hard, because your datamodel must be modelled as an TLIndexDataModel. I gave up on this.

@Timothy Moose 2014-08-28 14:57:43

@xaphod It does not actually. From the GitHub readme: "Requires TLIndexPathTools for internal implementation. The collection view itself does not necessarily need to use TLIndexPathTools, but the sample projects do."

@xaphod 2014-08-28 19:01:51

Yes, I read that too. In practice, it is not correct. Try it.

@Timothy Moose 2014-08-28 19:07:36

@xaphod I should mention that I wrote the library. I'll look into adding a sample project that doesn't use TLIPT. The only requirement for using this library is that you've got to implement the VCollectionViewGridLayoutDelegate delegate methods, which themselves don't require TLIPT. The main intent of this library, however, was to work around some animation issues with flow layout. The sticky headers feature is secondary, so there may be better options for those only interested in sticky headers.

@xaphod 2014-08-29 07:32:07

Ah ok -- well I did try hard NOT to use TLIPT at all, but I could see that in the layout class, that TLIPT was being used. I couldn't see around it. You are correct that all I'm after is sticky headers... but I haven't had any success yet (the above solution from cocotutch doesn't work for me either, indicating the problem is err, on my end)

@Derrick Hathaway 2013-06-04 16:45:53

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.

@Ortwin Gentz 2013-11-29 21:53:18

The header and footer views in this code behave differently compared to table view section headers and footers. They always stick even if scrolled farther than the end (so that it bounces).

@topLayoutGuide 2012-12-03 07:27:39

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.)

@Frank Schmitt 2013-02-01 22:50:46

This worked perfectly. Thanks!

@toblerpwn 2013-04-16 04:36:18

If numberOfItemsInSection == 0, you will get a EXC_ARITHMETIC code=EXC_I386_DIV (divide-by-zero?) crash on this line: UICollectionViewLayoutAttributes *firstCellAttrs = [self layoutAttributesForItemAtIndexPath:firstCellIndexPath];. I've also noticed that the contentOffset parts of this code are not respecting the collection view's top-inset value in my implementation (may be related to the previous issue).. Will propose a code change/gist/etc.

@toblerpwn 2013-04-16 05:12:22

gist.github.com/toblerpwn/5393460 - feel free to iterated, particularly on the limitations noted in the gist. :)

@Chris Blunt 2013-09-02 14:29:45

Great help, thanks. This worked perfectly with the PSTCollectionView compatibility classes by renaming the relevant classes (UICollectionView/PSTCollectionView, ... etc.) github.com/steipete/PSTCollectionView

@jerik 2013-11-11 13:18:35

Does not work for me, used your code by copy pasting. The headers are not sticky. My header is defined in the UICollectionViewFlowLayout class in the init method: self.headerReferenceSize = CGSizeMake(320,140).

@topLayoutGuide 2013-11-12 00:30:32

If it works for everybody else so there's something you're doing. Try declaring your headers in the appropriate Delegate method.

@mattsson 2013-11-20 15:36:33

Shouldn't this be doable without invalidating the layout on every bouns change?

@topLayoutGuide 2013-11-26 04:34:08

@mattsson I have no idea really. I just used what Apple gave me and came up with this. Feel free to alter it, but provide your edits here to avoid the creation of any more questions based on this subject :)

@mattsson 2013-12-10 08:41:59

Thanks. I ended up using a separate UICollectionView for the sticky header, since invalidating layout on every bounds change with a collection view containing 1000+ items destroys performance.

@louielouie 2014-03-17 21:48:26

Here's a nice writeup on this technique: blog.radi.ws/post/32905838158/…

@Andrew Raphael 2014-03-18 17:11:15

There is a bug in the code for the "if (numberOfItemsInSection > 0)" else clause situation. In that case the "- headerHeight" should not occur. I am willing to post the corrected code but am not sure a new answer is appropriate.

@topLayoutGuide 2014-03-19 06:55:19

Certainly it is appropriate. This solution isn't perfect, and does need improvement. Feel free :)

@xaphod 2014-08-28 14:34:50

I can't get this to work: in the [missingSections enumerateIndexesUsingBlock... part, often layoutAttributes is nil (it seems to get recursively called?). I am registering the class in the vc's ViewDidLoad, like this: [self.collectionView registerClass:[BHTimelineTitleReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionHea‌​der withReuseIdentifier:SectionTitleIdentifier];... any hints?

@Matt Baker 2014-09-19 14:50:48

@cocotutch have you encountered any issues with using this since the iOS 8 SDK launched? My cell layouts within this Flow are defaulting to a size of 50x50 even though I've defined collectionView:layout:sizeForItemAtIndexPath:. More specifically, the inner cell content is 50x50, but the cell's size is still correct. Only occurs when building against iOS 8 SDK but running on iOS 7.0.X. Building against iOS 7.1 SDK and running on 7.0.X works fine.

@Matt Baker 2014-09-19 15:05:22

@cocotutch looks like this: stackoverflow.com/questions/25804588/…

@user2273634 2013-04-12 09:02:22

I ran this with vigorouscoding's code. However that code did not consider sectionInset.

So I changed this code for vertical scroll

origin.y = MIN(
              MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)),
              (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
           );

to

origin.y = MIN(
           MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight - self.sectionInset.top)),
           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight + self.sectionInset.bottom)
           );

If you guys want code for horizontal scroll, refer to code aove.

@topLayoutGuide 2013-04-16 08:17:26

Thanks for adding Horizontal! :-) -coco

@vigorouscoding 2013-03-13 20:21:49

I ran into the same problem and found this in my google results. First I would like to thank cocotutch for sharing his solution. However, I wanted my UICollectionView to scroll horizontally and the headers to stick to the left of the screen, so I had to change the solution a bit.

Basically I just changed this:

        CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
        CGPoint origin = layoutAttributes.frame.origin;
        origin.y = MIN(
                       MAX(
                           contentOffset.y,
                           (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)
                           ),
                       (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
                       );

        layoutAttributes.zIndex = 1024;
        layoutAttributes.frame = (CGRect){
            .origin = origin,
            .size = layoutAttributes.frame.size
        };

to this:

        if (self.scrollDirection == UICollectionViewScrollDirectionVertical) {
            CGFloat headerHeight = CGRectGetHeight(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.y = MIN(
                           MAX(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)),
                           (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };
        } else {
            CGFloat headerWidth = CGRectGetWidth(layoutAttributes.frame);
            CGPoint origin = layoutAttributes.frame.origin;
            origin.x = MIN(
                           MAX(contentOffset.x, (CGRectGetMinX(firstCellAttrs.frame) - headerWidth)),
                           (CGRectGetMaxX(lastCellAttrs.frame) - headerWidth)
                           );

            layoutAttributes.zIndex = 1024;
            layoutAttributes.frame = (CGRect){
                .origin = origin,
                .size = layoutAttributes.frame.size
            };
        }

See: https://gist.github.com/vigorouscoding/5155703 or http://www.vigorouscoding.com/2013/03/uicollectionview-with-sticky-headers/

@MikeV 2013-01-23 21:55:38

If you have a single header view that you want pinned to the top of your UICollectionView, here's a relatively simple way to do it. Note this is meant to be as simple as possible - it assumes you are using a single header in a single section.

//Override UICollectionViewFlowLayout class
@interface FixedHeaderLayout : UICollectionViewFlowLayout
@end

@implementation FixedHeaderLayout
//Override shouldInvalidateLayoutForBoundsChange to require a layout update when we scroll 
- (BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

//Override layoutAttributesForElementsInRect to provide layout attributes with a fixed origin for the header
- (NSArray *) layoutAttributesForElementsInRect:(CGRect)rect {

    NSMutableArray *result = [[super layoutAttributesForElementsInRect:rect] mutableCopy];

    //see if there's already a header attributes object in the results; if so, remove it
    NSArray *attrKinds = [result valueForKeyPath:@"representedElementKind"];
    NSUInteger headerIndex = [attrKinds indexOfObject:UICollectionElementKindSectionHeader];
    if (headerIndex != NSNotFound) {
        [result removeObjectAtIndex:headerIndex];
    }

    CGPoint const contentOffset = self.collectionView.contentOffset;
    CGSize headerSize = self.headerReferenceSize;

    //create new layout attributes for header
    UICollectionViewLayoutAttributes *newHeaderAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader withIndexPath:[NSIndexPath indexPathForItem:0 inSection:0]];
    CGRect frame = CGRectMake(0, contentOffset.y, headerSize.width, headerSize.height);  //offset y by the amount scrolled
    newHeaderAttributes.frame = frame;
    newHeaderAttributes.zIndex = 1024;

    [result addObject:newHeaderAttributes];

    return result;
}
@end

See: https://gist.github.com/4613982

@Eusthace 2016-03-31 22:18:43

How to use it? :S

Related Questions

Sponsored Content

30 Answered Questions

[SOLVED] UITableView - change section header color

6 Answered Questions

[SOLVED] UICollectionView Multiple Sections and Headers

2 Answered Questions

[SOLVED] UIcollectionview decoration view VS supplementary view

0 Answered Questions

UICollectionView supplementary view from child controller

1 Answered Questions

[SOLVED] UICollectionView can't collapse or remove supplementary view

0 Answered Questions

1 Answered Questions

Sponsored Content