UICollectionView sticky header in swift

2020-01-30 04:11发布

I'm trying to create a sticky supplementary header, which stays on top all the time and won't response to scrolling events. The solutions I found so far still react on bounch scrolling and are fixed using a custom flowLayout, which will probably be the fix for mine issue as well.

The reason I want it this way is that the header is used on other places and should be reusable. I'm hoping this could be solved this way and I don't have to create a separated view.

As I'm doing this in Swift, it would be great to have an example in Swift.

9条回答
【Aperson】
2楼-- · 2020-01-30 04:32

Cleaner Swift 2.3 tested version of Irina's answer

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

    guard var superAttributes = super.layoutAttributesForElementsInRect(rect) else {
        return super.layoutAttributesForElementsInRect(rect)
    }

    let contentOffset = collectionView!.contentOffset
    let missingSections = NSMutableIndexSet()

    for layoutAttributes in superAttributes {
        if (layoutAttributes.representedElementCategory == .Cell) {
            missingSections.addIndex(layoutAttributes.indexPath.section)
        }

        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                missingSections.removeIndex(layoutAttributes.indexPath.section)
            }
        }
    }

    missingSections.enumerateIndexesUsingBlock { idx, stop in
        let indexPath = NSIndexPath(forItem: 0, inSection: idx)
        if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) {
            superAttributes.append(layoutAttributes)
        }
    }

    for layoutAttributes in superAttributes {
        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                let section = layoutAttributes.indexPath.section
                let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
                let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)

                var firstCellAttributes:UICollectionViewLayoutAttributes
                var lastCellAttributes:UICollectionViewLayoutAttributes

                if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                    firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
                    lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
                } else {
                    firstCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath)!
                    lastCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath)!
                }

                let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                var origin = layoutAttributes.frame.origin

                origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
                ;

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

            }
        }
    }

    return superAttributes
}
查看更多
Lonely孤独者°
3楼-- · 2020-01-30 04:35

Simplest solution for iOS 9 + as it doesn't need of writing subclass of UICollectionViewFlowLayout.

In viewDidLoad of viewController with collectionView use following code:

let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout // casting is required because UICollectionViewLayout doesn't offer header pin. Its feature of UICollectionViewFlowLayout
layout?.sectionHeadersPinToVisibleBounds = true

It is hinted by @Antoine also.

查看更多
相关推荐>>
4楼-- · 2020-01-30 04:41

I fixed my crash by testing for testing for empty sections as suggested in this gist. I also added a couple of if lets for extra Swift style points ;-). This now works for me:

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {

        var answer: [UICollectionViewLayoutAttributes] = super.layoutAttributesForElementsInRect(rect)! as [UICollectionViewLayoutAttributes]
        let contentOffset = collectionView!.contentOffset

        var missingSections = NSMutableIndexSet()

        for layoutAttributes in answer {
            if (layoutAttributes.representedElementCategory == .Cell) {
                if let indexPath = layoutAttributes.indexPath {
                    missingSections.addIndex(layoutAttributes.indexPath.section)
                }
            }
        }

        for layoutAttributes in answer {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    if let indexPath = layoutAttributes.indexPath {
                        missingSections.removeIndex(indexPath.section)
                    }
                }
            }
        }

        missingSections.enumerateIndexesUsingBlock { idx, stop in
            let indexPath = NSIndexPath(forItem: 0, inSection: idx)
            if let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath) {
                answer.append(layoutAttributes)
            }
        }

        for layoutAttributes in answer {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    let section = layoutAttributes.indexPath!.section
                    let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                    let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)!
                    let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)!


                    let (firstCellAttributes: UICollectionViewLayoutAttributes, lastCellAttributes: UICollectionViewLayoutAttributes) = {
                        if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                            return (
                                self.layoutAttributesForItemAtIndexPath(firstCellIndexPath),
                                self.layoutAttributesForItemAtIndexPath(lastCellIndexPath))
                        } else {
                            return (
                                self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath),
                                self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath))
                        }
                    }()

                    let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                    var origin = layoutAttributes.frame.origin

                    origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))

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

        return answer
    }

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

}
查看更多
戒情不戒烟
5楼-- · 2020-01-30 04:45

Swift 3 version trying to avoid !'s where it made sense.

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard var superAttributes = super.layoutAttributesForElements(in: rect), let collectionView = collectionView else {
            return super.layoutAttributesForElements(in: rect)
        }

        let collectionViewTopY = collectionView.contentOffset.y + collectionView.contentInset.top
        let contentOffset = CGPoint(x: 0, y: collectionViewTopY)
        let missingSections = NSMutableIndexSet()

        superAttributes.forEach { layoutAttributes in
            if layoutAttributes.representedElementCategory == .cell && layoutAttributes.representedElementKind != UICollectionElementKindSectionHeader {
                missingSections.add(layoutAttributes.indexPath.section)
            }
        }

        missingSections.enumerate(using: { idx, stop in
            let indexPath = IndexPath(item: 0, section: idx)
            if let layoutAttributes = self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: indexPath) {
                superAttributes.append(layoutAttributes)
            }
        })

        for layoutAttributes in superAttributes {
            if let representedElementKind = layoutAttributes.representedElementKind {
                if representedElementKind == UICollectionElementKindSectionHeader {
                    let section = layoutAttributes.indexPath.section
                    let numberOfItemsInSection = collectionView.numberOfItems(inSection: section)

                    let firstCellIndexPath = IndexPath(item: 0, section: section)
                    let lastCellIndexPath = IndexPath(item: max(0, (numberOfItemsInSection - 1)), section: section)                   

                    let cellAttributes:(first: UICollectionViewLayoutAttributes, last: UICollectionViewLayoutAttributes) = {
                        if (collectionView.numberOfItems(inSection: section) > 0) {
                            return (
                                self.layoutAttributesForItem(at: firstCellIndexPath)!,
                                self.layoutAttributesForItem(at: lastCellIndexPath)!)
                        } else {
                            return (
                                self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionHeader, at: firstCellIndexPath)!,
                                self.layoutAttributesForSupplementaryView(ofKind: UICollectionElementKindSectionFooter, at: lastCellIndexPath)!)
                        }
                    }()

                    let headerHeight = layoutAttributes.frame.height
                    var origin = layoutAttributes.frame.origin
                    // This line makes only one header visible fixed at the top
//                    origin.y = min(contentOffset.y, cellAttributes.last.frame.maxY - headerHeight)
                    // Uncomment this line for normal behaviour:
                    origin.y = min(max(contentOffset.y, cellAttributes.first.frame.minY - headerHeight), cellAttributes.last.frame.maxY - headerHeight)

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

        return superAttributes
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}
查看更多
倾城 Initia
6楼-- · 2020-01-30 04:46

works for me with swift 2.0

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

    var superAttributes:NSMutableArray = NSMutableArray(array: super.layoutAttributesForElementsInRect(rect)!) as NSMutableArray

    let contentOffset = collectionView!.contentOffset
    var missingSections = NSMutableIndexSet()

    for layoutAttributes in superAttributes {
        if (layoutAttributes.representedElementCategory == .Cell) {
            if let indexPath = layoutAttributes.indexPath {
                missingSections.addIndex(layoutAttributes.indexPath.section)
            }
        }
    }

    for layoutAttributes in superAttributes{
        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                if let indexPath = layoutAttributes.indexPath {
                    missingSections.removeIndex(indexPath.section)
                }
            }
        }
    }

    missingSections.enumerateIndexesUsingBlock { idx, stop in
        let indexPath = NSIndexPath(forItem: 0, inSection: idx)
        let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)
        superAttributes.addObject(layoutAttributes!)
    }

    for la in superAttributes {

        let layoutAttributes = la as! UICollectionViewLayoutAttributes;

        if let representedElementKind = layoutAttributes.representedElementKind {
            if representedElementKind == UICollectionElementKindSectionHeader {
                let section = layoutAttributes.indexPath.section
                let numberOfItemsInSection = collectionView!.numberOfItemsInSection(section)

                let firstCellIndexPath = NSIndexPath(forItem: 0, inSection: section)
                let lastCellIndexPath = NSIndexPath(forItem: max(0, (numberOfItemsInSection - 1)), inSection: section)                    

                var firstCellAttributes:UICollectionViewLayoutAttributes
                var lastCellAttributes:UICollectionViewLayoutAttributes

                    if (self.collectionView!.numberOfItemsInSection(section) > 0) {
                            firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
                            lastCellAttributes = self.layoutAttributesForItemAtIndexPath(lastCellIndexPath)!
                    } else {
                            firstCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: firstCellIndexPath)!
                            lastCellAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionFooter, atIndexPath: lastCellIndexPath)!
                    }

                let headerHeight = CGRectGetHeight(layoutAttributes.frame)
                var origin = layoutAttributes.frame.origin

                 origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttributes.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
                ;

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

            }
        }
    }

    return NSArray(array: superAttributes) as? [UICollectionViewLayoutAttributes]
}

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    return true
}
查看更多
爷、活的狠高调
7楼-- · 2020-01-30 04:47

i use this in my project. Hope it helps.

    func collectionView(collectionView: UICollectionView,
    layout collectionViewLayout: UICollectionViewLayout,
    referenceSizeForHeaderInSection section: Int) -> CGSize
{
    return CGSizeMake(UIScreen.mainScreen().bounds.width, 40)
}

of course. you can return whatever size you want.

查看更多
登录 后发表回答