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条回答
淡お忘
2楼-- · 2020-01-30 04:51

The final solution I found:

Using this custom flow layout it was possible to fix this sticky header:

class StickyHeaderCollectionViewFlowLayout: UICollectionViewFlowLayout {

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

        var superAttributes: [UICollectionViewLayoutAttributes]? = super.layoutAttributesForElementsInRect(rect) as? [UICollectionViewLayoutAttributes]

        if superAttributes == nil {
            // If superAttributes couldn't cast, return
            return super.layoutAttributesForElementsInRect(rect)
        }

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


                    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(contentOffset.y, (CGRectGetMaxY(lastCellAttributes.frame) - headerHeight))
                    // Uncomment this line for normal behaviour:
                    // 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
    }

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

}

To create a layout where the headers are sticky like traditional, change this line:

origin.y = min(contentOffset.y, (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))

to this line:

origin.y = min(max(contentOffset.y, (CGRectGetMinY(firstCellAttrs.frame) - headerHeight)), (CGRectGetMaxY(lastCellAttrs.frame) - headerHeight))

Hoping this is useful for others!

Update

Updated to fix a crash (thanks to Robert Atkins!) and some updates to Swift 1.2

tvOS & iOS 9

tvOS and iOS 9 introduced the property sectionHeadersPinToVisibleBounds which can be used

查看更多
贼婆χ
3楼-- · 2020-01-30 04:53

Swift 2.2 version tested based on GregP's answer. Greg's code was throwing an unwrap optional error at lastCellIndexPath because the number of section is initially zero. So I moved the numberOfItemsInSection > 0 check up.

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

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

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

    for layoutAttributes in superAttributes {
        if (layoutAttributes.representedElementCategory == .Cell) {
            if let _ = 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)

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

                    var firstCellAttributes:UICollectionViewLayoutAttributes
                    var lastCellAttributes:UICollectionViewLayoutAttributes

                    firstCellAttributes = self.layoutAttributesForItemAtIndexPath(firstCellIndexPath)!
                    lastCellAttributes = self.layoutAttributesForItemAtIndexPath(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
}
查看更多
女痞
4楼-- · 2020-01-30 04:54

Here is a simple solution that works if you only have one section.

class StickyHeaderLayout: UICollectionViewFlowLayout {

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

  override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
    var attributes = super.layoutAttributesForElementsInRect(rect)! as! [UICollectionViewLayoutAttributes]

      let offset          = collectionView?.contentOffset


      for attrs in attributes {
        if attrs.representedElementKind == nil {
          let indexPath        = NSIndexPath(forItem: 0, inSection: attrs.indexPath.section)
          let layoutAttributes = self.layoutAttributesForSupplementaryViewOfKind(UICollectionElementKindSectionHeader, atIndexPath: indexPath)

          attributes.append(layoutAttributes)
        }
      }

      for attrs in attributes {
        if attrs.representedElementKind == nil {
          continue
        }

        if attrs.representedElementKind == UICollectionElementKindSectionHeader {

          var headerRect = attrs.frame
          headerRect.size.height = headerHeight
          headerRect.origin.y = offset!.y
          attrs.frame = headerRect
          attrs.zIndex = 1024
          break
        }
      }

    return attributes
  }
}
查看更多
登录 后发表回答