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?

- 13,044
- 8
- 95
- 91

- 233
- 1
- 3
- 7
-
I never tried it practically but maybe you can try `frame.x == 0`? – Ben Ong Jan 16 '17 at 05:55
-
2There 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 scrolled – Ben Ong Jan 16 '17 at 06:05
-
thank you very much – Pakpoom Thaweesitthichat Jan 16 '17 at 06:23
-
improved the title and simplified the explanation – budiDino Jan 19 '17 at 11:55
-
@BenOng I suggest you post your comment as an answer – budiDino Jan 19 '17 at 12:03
5 Answers
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")
}
}

- 152
- 1
- 9
-
2
-
1This 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. – heyfrank Dec 17 '20 at 14:59
-
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.

- 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
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

- 11
- 2
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
}

- 41
- 5
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)
}
}
}

- 7,737
- 3
- 47
- 52