Detect when UITableView section header snaps to the top of the screen
Asked Answered
L

5

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?

Lakes answered 16/1, 2017 at 5:47 Comment(5)
I never tried it practically but maybe you can try frame.x == 0?Balcer
There is a rectForHeader method which you can use to get the frame of the header for that section, then there is a contentOffset property of UItableView. I believe you can calculate to find out if the header is at top of screen. There is also UIScrollViewdelegate method scrollViewDidScroll which you can use to check when the UITableView is scrolledBalcer
thank you very muchLakes
improved the title and simplified the explanationDiamagnetic
@BenOng I suggest you post your comment as an answerDiamagnetic
H
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")
    }
}
Hayrack answered 14/7, 2018 at 21:54 Comment(3)
Great and robust solution!Artistry
This is the only way that worked out for me. Measuring things in scroll view did scroll and converting the rect to the superview coordinate space was never working when scrolling fast.Wobble
This doesn't seem to work if you only have one section header.Charlena
B
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.

Balcer answered 20/1, 2017 at 2:4 Comment(2)
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 }Rambo
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 momentBalcer
E
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

Eucalyptus answered 5/12, 2020 at 14:5 Comment(0)
N
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
}
Ninebark answered 22/8, 2021 at 14:19 Comment(0)
M
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)
            }
        }
    }

Mcgrody answered 3/2, 2022 at 14:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.