Creating a sticky UICollectionViewController header that also stretches on scroll down

Using a UICollectionViewController I am attempting to create a section header that is sticky, by that I mean on scroll up it is fixed in position, with my UICollectionViewCell 's scrolling beneath it.

I would then like, on scroll down, to have the header stick in position, but stretch as the user pulls.

I can achieve both this individually but have been unable to combine these effects.

StickyHeader

fileprivate func configureCollectionViewLayout() {
    if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
        let padding: CGFloat = 16
        layout.sectionInset = .init(top: padding, left: padding, bottom: padding, right: padding)
        layout.sectionHeadersPinToVisibleBounds = true
    }
}

StretchyHeader

class StretchyHeaderLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let layoutAttributes = super.layoutAttributesForElements(in: rect)

        layoutAttributes?.forEach({ (attributes) in

            if attributes.representedElementKind == UICollectionView.elementKindSectionHeader && attributes.indexPath.section == 0 {
                guard let collectionView = collectionView else { return }

                let contentOffsetY = collectionView.contentOffset.y

                if contentOffsetY > 0 { return }

                let width = collectionView.frame.width
                let height = attributes.frame.height - contentOffsetY

                attributes.frame = CGRect(x: 0, y: contentOffsetY, width: width, height: height)
            }
        })

        return layoutAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}

Using the StretchyHeaderLayout I create my UICollectionViewController as FeedController(collectionViewLayout: StretchyHeaderLayout())

I then set a custom header view using

   override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "headerId", for: indexPath)
        return header
    }

My header view is simply view that consumes a few extensions to provide a UIImage based on a colour hex and also anchor that image on all 4 corners

class PageHeaderView: UICollectionReusableView {

    let headerBgView: UIImageView = {
        let iv = UIImageView(image: UIImage.from(hex: "ffffff"))
        return iv
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        addSubview(headerBgView)
        headerBgView.fillSuperview()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Please let me know if these extensions would aid your input.

I have tried the following in my UICollectionViewFlowLayout subclass

        if contentOffsetY >= 0 {
            sectionHeadersPinToVisibleBounds = true
            return
        } else {
            sectionHeadersPinToVisibleBounds = false
        }

However I get a crash with the error:

2019-01-06 07:54:36.321311+0000 Home[5596:314228] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore_Sim/UIKit-3698.93.8/UICollectionViewData.m:459

2019-01-06 07:54:36.324992+0000 Home[5596:314228] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'layout attributes for supplementary item at index path (<NSIndexPath: 0xa6c202f659ceada5> {length = 2, path = 0 - 0}) changed from <UICollectionViewLayoutAttributes: 0x7fa068414d50> index path: (<NSIndexPath: 0xa6c202f659ceada5> {length = 2, path = 0 - 0}); element kind: (UICollectionElementKindSectionHeader); frame = (0 0; 320 100); zIndex = 10; to <UICollectionViewLayoutAttributes: 0x7fa068419a60> index path: (<NSIndexPath: 0xa6c202f659ceada5> {length = 2, path = 0 - 0}); element kind: (UICollectionElementKindSectionHeader); frame = (0 -11.5; 320 111.5); zIndex = 10; without invalidating the layout'

I believe I have achieved this now, I implemented

override func scrollViewDidScroll(_ scrollView: UIScrollView) {
    guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { return }
    let offsetY = scrollView.contentOffset.y

    layout.sectionHeadersPinToVisibleBounds = offsetY > 0
}

Please let me know if you see any issues with this approach :)

I know this is not an answer to your question, but instead a question. When you say you're using a section header. Does this mean it is possible for me to do this via storyboard? Implement a collection view, add a section header and so on?

It is extremely easy to do this with a table view, collection views however is another beast..

Terms of Service

Privacy Policy

Cookie Policy