iOS/Objective-C: Sticky Headers using Regular UICollectionViewCells

If you’re looking for a way to create sticky headers for cells that use supplementary views, this is not what this tutorial is for. Instead, this tutorial outlines how to use generic UICollectionViewCells to achieve the same result.

Sumedha Mehta
5 min readJan 29, 2020

If you were looking for a tutorial for creating sticky headers using an implementation of a generic UICollectionView without any supplementary views — let’s get started:

Sticky Headers

Visually, sticky headers look something like this —

Notice that the headers are sticking to the top of the collection view while other cells slide underneath it. If you want to pick and choose certain cells in your collection view to stick to the top, you will need to implement 3 main chunks of code

  1. A helper function to return your sticky cells — what makes your “header”/”sticky” cells special? Is it a defining index? A cell type? Some text? Create a helper function to do this for you and return an array or set of index paths that represent these cells.
  2. Create a custom flow layout that subclasses UICollectionViewFlowLayout — by overriding certain methods, we can customize the behavior of certain cells (in this case, our sticky cells).
  3. Make your collection view use your custom layout — pretty straightforward.

Step 1: A helper function to return your sticky cells

My helper function is extremely basic. But I can see use cases involving casing on the type of a cell, the size of a cell, the text in a cell, using the data source for vital information, etc.

-(NSArray<NSIndexPath *> *)stickyCellIndexes {
return @[[NSIndexPath indexPathForRow:0 inSection:0], [NSIndexPath indexPathForRow:0 inSection:4],
[NSIndexPath indexPathForRow:0 inSection:8],
[NSIndexPath indexPathForRow:0 inSection:12]];
}

Once you are confident your function can return the cells we want to make sticky, let’s get to the meat of the coding.

Step 2: A helper function to return your sticky cells

We will need to subclass UICollectionViewFlowLayout. In order to do this, we will create our own type in the same header file that holds the collection view:

.h file for View Controller

// SomeViewController.h// implement this!@interface MyCustomCollectionViewFlowLayout : UICollectionViewFlowLayout- (instancetype)init NS_DESIGNATED_INITIALIZER;@end@interface SomeViewController : UIViewController- (instancetype)init... // the rest of your VC interface code that you already implemented
@end

.m file for View Controller

We will be overriding 2 methods in the .m file:

- (nullable NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds

The second is easy — just return YES;

@implementation MyCustomCollectionViewFlowLayout {
NSOrderedSet<NSIndexPath *> *_stickyIndexes;
}
#pragma mark - Overrides- (instancetype)init {
if (self = [super init]) {
_stickyIndexes = [NSOrderedSet new];
}
return self;
}
- (nullable NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Override This!
}
- (UICollectionViewLayoutAttributes *)_stickyAttributeIfNeeded:(UICollectionViewLayoutAttributes *)attributes {
// Implement This Helper Function!
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return YES;
}
-(NSArray<NSIndexPath *> *)stickyCellIndexes {
return @[[NSIndexPath indexPathForRow:0 inSection:0], [NSIndexPath indexPathForRow:0 inSection:4],
[NSIndexPath indexPathForRow:0 inSection:8],
[NSIndexPath indexPathForRow:0 inSection:12]];
}@end

Implementing layoutAttributesForElementsInRect

The logic behind this function is simple. We we are going to:

  1. Grab the collection view cells in the current rect
  2. Grab the attributes of the elements in the current rect (note — attributes are properties of each element in a collection view, such as size, shape, position, z-index, etc) by calling the super class. This way we can identify what the properties would ideally be if we weren’t creating a custom layout
  3. Loop through all the elements in the current rect (which we just stored). Look for the ones that match our sticky indexes.
  4. If they match — make the attribute sticky (explained in the next section).
- (nullable NSArray<__kindof UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
// Get the area we care about in our collection view
CGRect collectionViewArea = CGRectMake(0.0, 0.0, self.collectionView.contentSize.width, self.collectionView.contentSize.height);
// Grab the attributes of the elements in the current rect
NSMutableArray *attributesForElementsInRect = [[super layoutAttributesForElementsInRect:collectionViewArea] mutableCopy];
// Loop through all the elements, check if they match the index of any of our sticky indexes.
for (NSUInteger idx=0; idx<[attributesForElementsInRect count]; idx++) {
UICollectionViewLayoutAttributes *const layoutAttributes = attributesForElementsInRect[idx];
_stickyIndexes = [self stickyCellIndexes]
if ([_stickyIndexes containsObject:layoutAttributes.indexPath]) {
// If they match the sticky index, let's change the attributes
UICollectionViewLayoutAttributes *const stickyAttributes = [self _stickyAttributeIfNeeded:layoutAttributes];
attributesForElementsInRect[idx] = stickyAttributes;
}
}
return attributesForElementsInRect;
}

You’ll notice that we still need to implement our helper function

- (UICollectionViewLayoutAttributes *)_stickyAttributeIfNeeded:(UICollectionViewLayoutAttributes *)attributes;

This function is responsible for pinning the sticky cell to the top and increasing it’s z-index (the “height” off the screen of the cell which helps other cells slide under it).

Again, we break down the logic into a few steps:

  1. We get the top of the collection view so we know the y value of the a regular sticky index.
  2. We get the height and y value of our sticky cell attributes that have been passed in
  3. We get the height and y value of our next sticky cell, so we can slide up our current one if needed
  4. If our next sticky cell is still somewhere at the bottom, we will increase the z-index of our current cell and make sure the y value of the origin is at the top of our collection view.
  5. If our next sticky cell is colliding with our current sticky cell, slowly slide it off-screen.
  6. Handle the edge case of our last sticky cell.
- (UICollectionViewLayoutAttributes *)_stickyAttributeIfNeeded:(UICollectionViewLayoutAttributes *)attributes {// Get the top of the collection view    const CGFloat collectionViewTop = self.collectionView.contentOffset.y + self.collectionView.contentInset.top;// Get the height and Y value of the origin of the current cell
const CGFloat attributesHeight = CGRectGetHeight(attributes.frame);
const CGFloat attributesY = attributes.frame.origin.y;
// If the cell has scrolled up close enough to the top, start modifying it's attributes because it's a valid sticky cell.
if (collectionViewTop + attributesHeight > attributesY) {

const NSUInteger attributesIndex = [_stickyIndexes indexOfObject:attributes.indexPath];
// grab the next sticky index's attributes
if (attributesIndex < _stickyIndexes.count - 1) {
UICollectionViewLayoutAttributes *const nextAttributesIndex = [[super layoutAttributesForItemAtIndexPath:_stickyIndexes[attributesIndex + 1]] copy];

const CGFloat nextAttributesIndexY = CGRectGetMinY(nextAttributesIndex.frame);
// if the next attribute's index is still towards the bottom, just pin our current sticky index to the top like normalif (nextAttributesIndexY >= collectionViewTop + attributesHeight) {
CGRect frame = attributes.frame;
frame.origin.y = MAX(collectionViewTop, attributesY);
attributes.frame = frame;
attributes.zIndex = 1.0f;
}
// otherwise, start sliding up our current sticky index to make room for the next one -- see image in next section
else {
CGRect frame = attributes.frame;
frame.origin.y = MIN(collectionViewTop, nextAttributesIndexY - attributesHeight);
attributes.frame = frame;
attributes.zIndex = 1.0f;
}
}
} else if (attributesIndex == stickyIndexes.count - 1) {
// handle the last sticky index edge case
CGRect frame = attributes.frame;
frame.origin.y = MAX(collectionViewTop, attributesY);
attributes.frame = frame;
attributes.zIndex = 1.0f;
} return attributes;
}

One edge case that this code handles (in the middle else case) is that it slides our initial header up as the new one slides in, as opposed to immediately replacing it.

Step 3: Hooking up the custom flow layout to your Collection View

This part is easy!

_flowLayout = [MyCustomCollectionViewFlowLayout new];
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_flowLayout];

I hope this tutorial was able to concisely explain how to create “sticky” cells in your collection view. Any feedback, pointers, and questions are appreciated!

--

--

Sumedha Mehta

I like writing about StackOverflow answers I can’t find and other thoughts // product @ mongodb (realm)