Skip to content

Instantly share code, notes, and snippets.

@kylebshr
Created November 9, 2023 20:51
Show Gist options
  • Save kylebshr/0731820412aa60f9d0a85a9f0f8ac610 to your computer and use it in GitHub Desktop.
Save kylebshr/0731820412aa60f9d0a85a9f0f8ac610 to your computer and use it in GitHub Desktop.
UICollectionViewCompositionalLayout subclass that can stack a pinned collection-level header with pinned section-level headers.
class StackedHeaderCompositionalLayout: UICollectionViewCompositionalLayout {
let collectionHeaderKind: String
let sectionHeaderKind: String
init(
section: NSCollectionLayoutSection,
configuration: UICollectionViewCompositionalLayoutConfiguration,
collectionHeaderKind: String,
sectionHeaderKind: String
) {
self.collectionHeaderKind = collectionHeaderKind
self.sectionHeaderKind = sectionHeaderKind
super.init(section: section, configuration: configuration)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let layoutAttributes = super.layoutAttributesForElements(in: rect)
let collectionHeaderAttributes = layoutAttributes?.first(where: { $0.representedElementKind == collectionHeaderKind })
collectionHeaderAttributes?.zIndex = 100
let collectionHeaderFrame = collectionHeaderAttributes?.frame ?? .zero
layoutAttributes?.forEach { attributes in
guard attributes.representedElementKind == sectionHeaderKind else {
return
}
attributes.zIndex = 50
// The positition of the header to pin it to the top, while allowing it to scroll down.
let pinnedTop = max(collectionHeaderFrame.maxY, attributes.frame.origin.y)
let section = attributes.indexPath.section
let nextSection = IndexPath(row: 0, section: section + 1)
if let nextAttributes = layoutAttributesForSupplementaryView(ofKind: sectionHeaderKind, at: nextSection) {
let nextHeaderTop = nextAttributes.frame.origin.y
let offsetPinnedTop = min(pinnedTop, nextHeaderTop - attributes.frame.height)
attributes.frame.origin.y = offsetPinnedTop
} else {
attributes.frame.origin.y = pinnedTop
}
}
return layoutAttributes
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
true
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment