9

I want to detect when the UITableView section header snaps on top of the screen and then modify header's height, but I have no idea how to do that. Can anyone help?

budiDino
  • 13,044
  • 8
  • 95
  • 91

5 Answers5

13

I was able to accomplish detecting which header is stuck to top of the table view by using the didEndDisplayingHeaderView and willDisplayHeaderView delegate methods. The idea here is that the while we are scrolling through the sections of the table view, the rows around the header are becoming visible and we can grab the index paths of all visible rows using tableView.indexPathsForVisibleRows. Depending on if we are scrolling up or down, we compare the first or last indexPath on screen to the index path of the view that just appeared/disappeared.

//pushing up
func tableView(_ tableView: UITableView,
               didEndDisplayingHeaderView view: UIView,
               forSection section: Int) {

    //lets ensure there are visible rows.  Safety first!
    guard let pathsForVisibleRows = tableView.indexPathsForVisibleRows,
        let lastPath = pathsForVisibleRows.last else { return }

    //compare the section for the header that just disappeared to the section
    //for the bottom-most cell in the table view
    if lastPath.section >= section {
        print("the next header is stuck to the top")
    }

}

//pulling down
func tableView(_ tableView: UITableView,
               willDisplayHeaderView view: UIView,
               forSection section: Int) {

    //lets ensure there are visible rows.  Safety first!
    guard let pathsForVisibleRows = tableView.indexPathsForVisibleRows,
        let firstPath = pathsForVisibleRows.first else { return }

    //compare the section for the header that just appeared to the section
    //for the top-most cell in the table view
    if firstPath.section == section {
        print("the previous header is stuck to the top")
    }
}
imattice
  • 152
  • 1
  • 9
2

I went to make my comment into codes and it works like:

override func scrollViewDidScroll(_ scrollView: UIScrollView) {

    if scrollView == yourTableView {
        if yourTableView.rectForHeader(inSection: <#sectionIWant#>).origin.y <= tableView.contentOffset.y-defaultOffSet.y &&
            yourTableView.rectForHeader(inSection: <#sectionIWant#>+1).origin.y > tableView.contentOffset.y-defaultOffSet.y {
            print("Section Header I want is at top")
        }
    } else {
        //Handle other scrollViews here
    }
}

So you replace sectionIWant with an Int and when that section's header is at top, the print will run.Note that yourTableView is not a parameter of scrollViewDidScroll so you have to keep a reference to your UITableView elsewhere and use it here.

Alternatively if you want to be constantly updated on which section header is at top you can use:

override func scrollViewDidScroll(_ scrollView: UIScrollView) {

    if scrollView == yourTableView {
        var currentSection = 0
        while !(tableView.rectForHeader(inSection: currentSection).origin.y <= (tableView.contentOffset.y-defaultOffSet.y) &&
            tableView.rectForHeader(inSection: currentSection+1).origin.y > (tableView.contentOffset.y)-defaultOffSet.y) {
                if tableView.contentOffset.y <= tableView.rectForHeader(inSection: 0).origin.y {
                    break   //Handle tableView header - if any
                }
                currentSection+=1
                if currentSection > tableView.numberOfSections-2 {
                    break   //Handle last section
                }
        }
        print(currentSection)
    }

}

Note that in both codes there is a defaultOffSet which is the contentOffSet of the tableView on load, this is to take into account that the contentOffSet does not start at 0 in some situations - I am not sure when but I do encounter such cases before.

Ben Ong
  • 913
  • 1
  • 10
  • 25
  • Actually, UITableView is a subclass of UIScrollView, so the scrollView passed in can be cast to your table view class `guard let tableView = scrollView as? UITableView else { return }` – John Dec 02 '19 at 19:57
  • Agreed but in most cases the table view is already an IBOutlet so you can just use that instead of adding another line to cast the scroll view, though I guess adding a check to make sure the table view is actually the one scrolling and not another scroll view should be done... I'll update my answer in a moment – Ben Ong Dec 03 '19 at 00:32
1

This solution works for me.

func scrollViewDidScroll(_ scrollView: UIScrollView) {
  self.visibleHeaders.forEach { header in
    let headerOriginalFrame = self.myTableView.rectForHeader(inSection: header.tag)
    let headerCurrentFrame = header.frame
    let fixed = headerOriginalFrame != headerCurrentFrame
    // Do things you want
  }
}

Here is full source code. https://github.com/jongwonwoo/CodeSamples/blob/master/TableView/TableviewFixedHeader/Tableview/ViewController.swift

jongwon
  • 11
  • 2
0

My solution to this problem turned out to be really working and versatile enough. I would be very glad if this helps someone

func scrollViewDidScroll(_ scrollView: UIScrollView) {
let sectionPosition = tableView.rect(forSection: sectionIndex)
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)

if let dataSource = dataSource,
   scrollView.contentOffset.y >= sectionPosition.minY {

//Next section

    guard dataSource.count - 1 > sectionIndex else { return }
    sectionIndex += 1
} else if translation.y > 0,
          scrollView.contentOffset.y <= sectionPosition.minY {

//Last section

    guard sectionIndex > 0 else { return }
    sectionIndex -= 1
}
Alex Bro
  • 41
  • 5
0

Late to the party, but reviewing up all responses didn't solve my issue.

The following approach, borrow a little bit from each one, adding at the same time, a more general and not fixed approach.

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard let visibleIndexs = self.tableView.indexPathsForVisibleRows,
              !visibleIndexs.isEmpty else { return }
        
        var visibleHeaders = [Int]()
        visibleIndexs.forEach { anIndexPath in
            if !visibleHeaders.contains(anIndexPath.section) {
                visibleHeaders.append(anIndexPath.section)
            }
        }
        
        visibleHeaders.forEach { header in
            let headerView = tableView.headerView(forSection: header)
            let headerOriginalFrame = self.tableView.rectForHeader(inSection: header)
            if headerOriginalFrame != headerView?.frame {
    //do something with top headerView
            } else {
    //do something else with all the others headerView(s)
            }
        }
    }

valvoline
  • 7,737
  • 3
  • 47
  • 52