Thanks again for your reply.
>-- since you're talking about sticky headers, i'm guessing you're using a Plain style UITableView. one question might be whether the same behaviour exists with a Grouped style?
I'm not sure. I really need sticky headers for this UI so I didn't try the grouped style.
>- - caching your own headers seems like more work than is needed, although that brings up a question about how you currently use tableView:viewForHeaderInSection, assuming that you do at all.
Indeed, caching my headers seems like more work than needed; this is the workaround I came up with to avoid the issue I described in the original post. I was not using tableView:viewForHeaderInSection at all, instead I was using -tableView:titleForHeaderInSection.
It looks like UITableView is removing the reference to a header view from its reuse queue but not cleaning it up from the view hierarchy. Then when the table view asks the data source for another title, (in tableView:viewForHeaderInSection:) it creates a new instance instead of re-using the header view already created, but the old header remains in the view hierarchy abandoned. This is why my workaround works, because I hold the reference to the original header view, and next time the table view asks for a header view, I give it the exact same instance. This happens periodically after removing a row via the swipe action (shown in the code posted above).
My cell layout is very complex, I suspect that may have something to do with it. Cells calculate their own height by implementing sizeThatFits: and the table view is set to use UITableViewAutomaticDimension for row height and estimated row height.
Also the table view periodically reloads cells when thumbnail images load (they are loaded lazily). Hard to say what could be triggering this though.
I'm pretty much out of ideas on this one too. Would be able to see what's going pretty quickly if Apple shared UITableView's source code 😁.