Stretchy UICollectionView headers

The bouncing effect of scroll views is probably one of the most distinctive things about iOS. While originally it was just eye candy over time it has actually inspired a few functional uses, like pull-to-refresh. Another good use of the scroll view bounce that I’ve seen lately is the stretchy header.

This effect is great. As you pull the scroll view down more of the image is subtly revealed on the top and bottom. You’ve probably seen this in the Twitter iOS app at the top of your feed as well as on Airbnb listings in their app. This effect is actually quite easy to implement and today I’m going to show you how to do it.

Let’s get started. First, create a subclass of UICollectionViewFlowLayout.

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

If you’ve ever played around with custom UICollectionView layouts then you’ve probably realized that they are quite powerful. Here is our implementation:

#import "StretchyHeaderCollectionViewLayout.h"
 
@implementation StretchyHeaderCollectionViewLayout
 
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    // This will schedule calls to layoutAttributesForElementsInRect: as the 
    // collectionView is scrolling. 
    return YES;
}
 
- (UICollectionViewScrollDirection)scrollDirection {
    // This subclass only supports vertical scrolling.
    return UICollectionViewScrollDirectionVertical;
}
 
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
 
    UICollectionView *collectionView = [self collectionView];
    UIEdgeInsets insets = [collectionView contentInset];
    CGPoint offset = [collectionView contentOffset];
    CGFloat minY = -insets.top;
 
    // First get the superclass attributes.
    NSArray *attributes = [super layoutAttributesForElementsInRect:rect];
 
    // Check if we've pulled below past the lowest position
    if (offset.y < minY) {  
 
        // Figure out how much we've pulled down 
        CGFloat deltaY = fabsf(offset.y - minY);
 
        for (UICollectionViewLayoutAttributes *attrs in attributes) {
 
            // Locate the header attributes
            NSString *kind = [attrs representedElementKind];
            if (kind == UICollectionElementKindSectionHeader) {
 
                // Adjust the header's height and y based on how much the user
                // has pulled down.
                CGSize headerSize = [self headerReferenceSize];
                CGRect headerRect = [attrs frame];
                headerRect.size.height = MAX(minY, headerSize.height + deltaY);
                headerRect.origin.y = headerRect.origin.y - deltaY;
                [attrs setFrame:headerRect];
                break;
            }
        }
    }
    return attributes;
}
 
@end

This custom layout will check to see if we’ve pulled down beyond the lowest possible offset, which means that we’re stretching it. If we are, locate the header element and increase its height and offset its y by the stretched amount.

Next, wherever you set up your collection view, add the following code:

// Create a new instance of our stretchy layout and set the 
// default size for our header (for when it's not stretched)
StretchyHeaderCollectionViewLayout *stretchyLayout;
stretchyLayout = [[StretchyHeaderCollectionViewLayout alloc] init];
[stretchyLayout setHeaderReferenceSize:CGSizeMake(320.0, 160.0)];
 
// Set our custom layout
[collectionView setCollectionViewLayout:stretchyLayout];
// and tell our collection view to always bounce.
[collectionView setAlwaysBounceVertical:YES];
 
// Then register a class to use for the header.
[collectionView registerClass:[UICollectionReusableView class]
   forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
          withReuseIdentifier:@"ident"];

The last thing we need to do is create our header which should be a UICollectionReusableView. We can add a UIImageView to it as a subview and use springs and struts to keep it sized properly.

- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView 
           viewForSupplementaryElementOfKind:(NSString *)kind 
                                 atIndexPath:(NSIndexPath *)indexPath {
    // You can make header an ivar so we only ever create one
    if (!header) {
 
        header = [collectionView dequeueReusableSupplementaryViewOfKind:kind
                                                    withReuseIdentifier:@"ident"
                                                           forIndexPath:indexPath];
        CGRect bounds;
        bounds = [header bounds];
 
        UIImageView *imageView;
        imageView = [[UIImageView alloc] initWithFrame:bounds];
        [imageView setImage:[UIImage imageNamed:@"header-background"]];
 
        // Make sure the contentMode is set to scale proportionally
        [imageView setContentMode:UIViewContentModeScaleAspectFill];
        // Clip the parts of the image that are not in frame
        [imageView setClipsToBounds:YES];
        // Set the autoresizingMask to always be the same height as the header
        [imageView setAutoresizingMask:UIViewAutoresizingFlexibleHeight];
        // Add the image to our header
        [header addSubview:imageView];
    }
    return header;
}

The autoresizingMask will keep the image height the same as the header. The contentMode and clipsToBounds properties will center the image and clip the rest while maintaining the aspect ratio.

You can view the full source and a sample project on Github.