0

I have a requirement in my project that I have to add shadow to UICollectionView headers when they become sticky.

I am using the property on UICollectionViewFlowLayout sectionHeadersPinToVisibleBounds, which when set to YES makes the headers sticky.

Can we get some sort of callback when the headers become sticky and actually pin to top so that I can add a shadow to that header? I don't want the shadow to be there when a header is not sticky.

The challenge here is, I can't really make use of the information in method- layoutAttributesForSupplementaryView: which does provide me the views on screen as my sections having dynamic no of items, hence which section is on screen doesn't really tell me who needs a shadow.

halfer
  • 19,824
  • 17
  • 99
  • 186
neha
  • 6,327
  • 12
  • 46
  • 78

2 Answers2

3

I've managed to find the solution to this question by:

  1. manually saving position of a header in:

    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { 
    let cell = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "CategoryNameReusableView", for: indexPath) as! CategoryNameReusableView
                cell.nameLbl.text = sectionNames[indexPath.section]
                if viewWithPosition.isEmpty { viewWithPosition.append(( name: cell.nameLbl.text!, position: cell.frame.origin.y))}
                let x = isElementInArray(array: viewWithPosition, string: cell.nameLbl.text!)
                if !x {
                    viewWithPosition.append((name: cell.nameLbl.text!, position: cell.frame.origin.y))
                }
            return cell
    }
    

I am using an array of viewWithPosition tuples var viewWithPosition = [(name: String, position: CGFloat)](). It might have been the same if i saved only position of a cell, but since my cells have different text i used name parameter to check if the element already exists in the array. I used helper function:

fileprivate func isElementInArray(array: [ViewAndPosition], string: String) -> Bool {
    var bool = false
    for views in array {
        if views.name == string {
            bool = true
            return bool
        }
    }
    return bool
}
  1. When i have initial positions i check if visible headers are misplaced:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if let visibleHeadersInSection = collectionView.visibleSupplementaryViews(ofKind: UICollectionElementKindSectionHeader) as? [CategoryNameReusableView] {
        for header in visibleHeadersInSection{
            if let index = viewWithPosition.index(where: { (element) -> Bool in
                element.name == header.nameLbl.text
            }){
                if header.frame.origin.y == viewWithPosition[index].position {
                    header.backgroundColor = UIColor.clear
                } else {
                    header.backgroundColor = UIColor.white
                }
            }
        }
    }
    

    }

At the moment it seems that there are no callbacks provided by Apple for this kind of situation. Hopefully this is also the solution for you.

IvanMih
  • 1,815
  • 1
  • 11
  • 23
  • I want to use "sectionHeadersPinToVisibleBounds" as sticky header , but when old header off screen , I want to change the new header color and update map annotation accordingly , your code is the one I can implement to do this . thanks a lot !!! – Richard Mao Jan 10 '19 at 10:40
0

I've modernized and improved the solution from IvanMih. Note that headerGroupsBeingDisplayed is the dataset being displayed in the UICollectionView:

// Array of tuples that stores data about all inflated headers. Used for conditionally changing sticky header color.
// The "position" is the original vertical position of the header view relative to its frame. When a header sticks to the top, the header view is no longer aligned with its frame.
// We exploit this lack of alignment to detect when a given header is stuck to the top.
var headerStringsAndOriginalPositions = [(name: String, position: CGFloat)]()


// Specify how Header views get inflated. Add a header to headerStringsAndOriginalPositions if not already present.
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    let cell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "DateHeaderReusableView", for: indexPath) as! DateHeaderReusableView
    let dateHeaderString = headerGroupsBeingDisplayed[indexPath.section].dateHeader
    cell.dateLabel.text = dateHeaderString
    if (!headerStringsAndOriginalPositions.contains(where: { name, position in name == dateHeaderString })) {
        headerStringsAndOriginalPositions.append((name: dateHeaderString, position: cell.frame.origin.y))
    }
    return cell
}


// Every time user scrolls, iterate through list of visible headers and check whether they're sticky or not. Then modify the header views accordingly.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if let visibleHeadersInSection = collectionView.visibleSupplementaryViews(ofKind: UICollectionView.elementKindSectionHeader) as? [DateHeaderReusableView] {
        for header in visibleHeadersInSection {
            if let index = headerStringsAndOriginalPositions.firstIndex(where: { (element) -> Bool in element.name == header.dateLabel.text }) {
                if (header.frame.origin.y == headerStringsAndOriginalPositions[index].position) {
                    // Header is non-sticky (as header view is aligned with its frame).
                    header.dateLabel.textColor = .red
                } else {
                    // Header is sticky (as header view is not aligned with its frame).
                    header.dateLabel.textColor = .blue
                }
            }
        }
    }
}
Gavin Wright
  • 3,124
  • 3
  • 14
  • 35