I'm trying to create a complex sticky UICollectionView header:
- it must be resizable based on specific scroll criteria, e.g. when a user scrolls past a certain
y
position, the header will resize. - it must automatically resize while the collection view content is scrolling
- it must respond to touch events on the header itself (it can't be a background view that "appears" like a header)
Now this has proven to be quite the challenge, but I've made some progress. To simplify the problem, I'm starting by just creating a header that will resize on scroll and keep its bounds when the touch ends.
To get started, I've created a collection view and supplementary view header that looks like this in its simplest form:
private let defaultHeaderViewHeight: CGFloat = 250.0
private var beginHeaderViewHeight: CGFloat = defaultHeaderViewHeight
private var currentHeaderViewHeight: CGFloat = defaultHeaderViewHeight
private var headerView: HeaderView? {
get {
guard let headerView = collectionView?.supplementaryView(forElementKind: UICollectionElementKindSectionHeader, at: IndexPath(row: 0, section: 0)) as? HeaderView else { return nil }
return headerView
}
}
// Resizing logic
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// Define the header size. This is called each time the layout is invalidated, and the beginHeaderViewHeight value is different each time
print("Header ref size delegate called, setting height to \(beginHeaderViewHeight)")
return CGSize(width: collectionView.frame.size.width, height: beginHeaderViewHeight)
// ^ super important!
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
currentHeaderViewHeight = max(0, -(collectionView?.contentOffset.y)! + beginHeaderViewHeight)
headerView?.frame.size.height = currentHeaderViewHeight
headerView?.frame.origin.y = collectionView?.contentOffset.y ?? 0
print("Did scroll\t\t\tcurrentHeaderViewHeight: \(currentHeaderViewHeight), content offset: \(scrollView.contentOffset.y)")
}
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
print("Will end dragging\t\theader view frame height: \(headerView?.frame.size.height), content offset: \(scrollView.contentOffset.y)")
scrollView.bounds.origin = CGPoint(x: 0, y: 0)
self.collectionView?.collectionViewLayout.invalidateLayout()
print("Will end dragging\t\theader view frame height: \(headerView?.frame.size.height), content offset: \(scrollView.contentOffset.y)")
beginHeaderViewHeight = currentHeaderViewHeight
}
// Basic UICollectionView setup
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.register(UINib(nibName: "HeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header View")
}
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 50
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
return cell
}
This kinda works, but not entirely. When I scroll "up" (touch tracks down), it behaves as I'd expect - when the touch ends, the header resizes and sticks in place. Here's a log extract to validate:
Did scroll currentHeaderViewHeight: 447.333333333333, content offset: -70.3333333333333
Did scroll currentHeaderViewHeight: 449.0, content offset: -72.0
Did scroll currentHeaderViewHeight: 451.0, content offset: -74.0
Did scroll currentHeaderViewHeight: 451.666666666667, content offset: -74.6666666666667
Will end dragging header view frame height: Optional(451.666666666667), content offset: -74.6666666666667
Will end dragging header view frame height: Optional(451.666666666667), content offset: 0.0
Did end dragging currentHeaderViewHeight: 451.666666666667, content offset: 0.0
Header ref size delegate called, setting height to 451.666666666667
But when I scroll "down" (touch tracks up), it gets really goofed: it appears that scrollViewDidScroll
is being called, even though I'm not setting the content offset anywhere:
Did scroll currentHeaderViewHeight: 330.666666666667, content offset: 121.0
Did scroll currentHeaderViewHeight: 330.333333333333, content offset: 121.333333333333
Did scroll currentHeaderViewHeight: 330.0, content offset: 121.666666666667
Did scroll currentHeaderViewHeight: 328.333333333333, content offset: 123.333333333333
Will end dragging header view frame height: Optional(328.333333333333), content offset: 123.333333333333
Will end dragging header view frame height: Optional(328.333333333333), content offset: 0.0
Did end dragging currentHeaderViewHeight: 328.333333333333, content offset: 0.0
Header ref size delegate called, setting height to 328.333333333333
// WHY ARE THESE LAST TWO LINES CALLED?
Did scroll currentHeaderViewHeight: 205.0, content offset: 123.333333333333
Did end deceler.. currentHeaderViewHeight: 205.0, content offset: 123.333333333333
So the tl;dr here: why on earth does it work when I scroll in one direction and not the other?
Bonus points: If there's a cleaner, simpler way to do all of this, feel free to share!