'Is it possible to disable floating headers in UITableView with UITableViewStylePlain?
I'm using a UITableView to layout content 'pages'. I'm using the headers of the table view to layout certain images etc. and I'd prefer it if they didn't float but stayed static as they do when the style is set to UITableViewStyleGrouped.
Other then using UITableViewStyleGrouped, is there a way to do this? I'd like to avoid using grouped as it adds a margin down all my cells and requires disabling of the background view for each of the cells. I'd like full control of my layout. Ideally they'd be a "UITableViewStyleBareBones", but I didn't see that option in the docs...
Many thanks,
Solution 1:[1]
A probably easier way to achieve this:
Objective-C:
CGFloat dummyViewHeight = 40;
UIView *dummyView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width, dummyViewHeight)];
self.tableView.tableHeaderView = dummyView;
self.tableView.contentInset = UIEdgeInsetsMake(-dummyViewHeight, 0, 0, 0);
Swift:
let dummyViewHeight = CGFloat(40)
self.tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: self.tableView.bounds.size.width, height: dummyViewHeight))
self.tableView.contentInset = UIEdgeInsets(top: -dummyViewHeight, left: 0, bottom: 0, right: 0)
Section headers will now scroll just like any regular cell.
Solution 2:[2]
(For who ever got here due to wrong table style) Change Table style from plain to grouped, via the attributes inspector, or via code:
let tableView = UITableView(frame: .zero, style: .grouped)
Solution 3:[3]
WARNING: this solution implements a reserved API method. This could prevent the app from being approved by Apple for distribution on the AppStore.
I've described the private methods that turns of section headers floating in my blog
Basically, you just need to subclass UITableView and return NO in two of its methods:
- (BOOL)allowsHeaderViewsToFloat;
- (BOOL)allowsFooterViewsToFloat;
Solution 4:[4]
In your Interface Builder click on your problem Table View
Then navigate to Attributes Inspector and change Style Plain to Grouped ;) Easy
Solution 5:[5]
Ok, i know it is late but i had to do it. I have spent 10 hours by now searching for a working solution but did not find a complete answer. Did found some hints but difficult for starters to understand. So i had to put in my 2 cents and complete the answer.
As it has been suggested in the few of the answers the only working solution that i was able to implement is by inserting normal cells in the table view and handle them as Section Headers, but the better way to achieve it is by inserting these cells at row 0 of every section. This way we can handle these custom non-floating headers very easily.
So, the steps are.
Implement UITableView with style UITableViewStylePlain.
-(void) loadView { [super loadView]; UITableView *tblView =[[UITableView alloc] initWithFrame:CGRectMake(0, frame.origin.y, frame.size.width, frame.size.height-44-61-frame.origin.y) style:UITableViewStylePlain]; tblView.delegate=self; tblView.dataSource=self; tblView.tag=2; tblView.backgroundColor=[UIColor clearColor]; tblView.separatorStyle = UITableViewCellSeparatorStyleNone; }Implement titleForHeaderInSection as usual ( you can get this value by using your own logic, but I prefer to use standard delegates ).
- (NSString *)tableView: (UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSString *headerTitle = [sectionArray objectAtIndex:section]; return headerTitle; }Immplement numberOfSectionsInTableView as usual
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { int sectionCount = [sectionArray count]; return sectionCount; }Implement numberOfRowsInSection as usual.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { int rowCount = [[cellArray objectAtIndex:section] count]; return rowCount +1; //+1 for the extra row which we will fake for the Section Header }Return 0.0f in heightForHeaderInSection.
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return 0.0f; }DO NOT implement viewForHeaderInSection. Remove the method completely instead of returning nil.
In heightForRowAtIndexPath. Check if(indexpath.row == 0) and return the desired cell height for the section header, else return the height of the cell.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if(indexPath.row == 0) { return 80; //Height for the section header } else { return 70; //Height for the normal cell } }Now in cellForRowAtIndexPath, check if(indexpath.row == 0) and implement the cell as you want the section header to be and set the selection style to none. ELSE implement the cell as you want the normal cell to be.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"SectionCell"]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"SectionCell"] autorelease]; cell.selectionStyle = UITableViewCellSelectionStyleNone; //So that the section header does not appear selected cell.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"SectionHeaderBackground"]]; } cell.textLabel.text = [tableView.dataSource tableView:tableView titleForHeaderInSection:indexPath.section]; return cell; } else { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"] autorelease]; cell.selectionStyle = UITableViewCellSelectionStyleGray; //So that the normal cell looks selected cell.backgroundView =[[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"CellBackground"]]autorelease]; cell.selectedBackgroundView=[[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"SelectedCellBackground"]] autorelease]; } cell.textLabel.text = [[cellArray objectAtIndex:indexPath.section] objectAtIndex:indexPath.row -1]; //row -1 to compensate for the extra header row return cell; } }Now implement willSelectRowAtIndexPath and return nil if indexpath.row == 0. This will care that didSelectRowAtIndexPath never gets fired for the Section header row.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) { return nil; } return indexPath; }And finally in didSelectRowAtIndexPath, check if(indexpath.row != 0) and proceed.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row != 0) { int row = indexPath.row -1; //Now use 'row' in place of indexPath.row //Do what ever you want the selection to perform } }
With this you are done. You now have a perfectly scrolling, non-floating section header.
Solution 6:[6]
Change your TableView Style:
self.tableview = [[UITableView alloc] initwithFrame:frame style:UITableViewStyleGrouped];
As per apple documentation for UITableView:
UITableViewStylePlain- A plain table view. Any section headers or footers are displayed as inline separators and float when the table view is scrolled.
UITableViewStyleGrouped- A table view whose sections present distinct groups of rows. The section headers and footers do not float.
Solution 7:[7]
The interesting thing about UITableViewStyleGrouped is that the tableView adds the style to the cells and not to the TableView.
The style is added as backgroundView to the cells as a class called UIGroupTableViewCellBackground which handles drawing different background according to the position of the cell in the section.
So a very simple solution will be to use UITableViewStyleGrouped, set the backgroundColor of the table to clearColor, and simply replace the backgroundView of the cell in cellForRow:
cell.backgroundView = [[[UIView alloc] initWithFrame:cell.bounds] autorelease];
Solution 8:[8]
There is another tricky way. The main idea is to double the section number, and first one only shows the headerView while the second one shows the real cells.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return sectionCount * 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section%2 == 0) {
return 0;
}
return _rowCount;
}
What need to do then is to implement the headerInSection delegates:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if (section%2 == 0) {
//return headerview;
}
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (section%2 == 0) {
//return headerheight;
}
return 0;
}
This approach also has little impact on your datasources:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
int real_section = (int)indexPath.section / 2;
//your code
}
Comparing with other approaches, this way is safe while not changing the frame or contentInsets of the tableview. Hope this may help.
Solution 9:[9]
For Swift 3+
Simply use UITableViewStyleGrouped and set the footer's height to zero with the following:
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return .leastNormalMagnitude
}
Solution 10:[10]
Although this may not solve your problem, it did for me when I wanted to do a similar thing. Instead of setting the header I used the footer of the section above. What saved me was that this section was small and static in nature, so it never scrolled below the bottom of the view.
Solution 11:[11]
The easiest way to get what you want is set your table style as UITableViewStyleGrouped,
separator style as UITableViewCellSeparatorStyleNone:
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return CGFLOAT_MIN; // return 0.01f; would work same
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
return [[UIView alloc] initWithFrame:CGRectZero];
}
Do not try return footer view as nil, don't forget set header height and header view, after you must get what you desired.
Solution 12:[12]
Maybe you could use scrollViewDidScroll on the tableView
and change the contentInset based on the current visible header.
It seems to work for my use case!
extension ViewController : UIScrollViewDelegate{
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let tableView = scrollView as? UITableView,
let visible = tableView.indexPathsForVisibleRows,
let first = visible.first else {
return
}
let headerHeight = tableView.rectForHeader(inSection: first.section).size.height
let offset = max(min(0, -tableView.contentOffset.y), -headerHeight)
self.tableView.contentInset = UIEdgeInsetsMake(offset, 0, -offset, 0)
}
}
Solution 13:[13]
While thinking how to approach this problem, I remembered a very important detail about UITableViewStyleGrouped. The way UITableView implements the grouped style (the rounded borders around the cells) is by adding a custom backgroundView to the UITableViewCells, and not to the UITableView. Each cell is added a backgroundView according to its position in the section (upper rows get the upper part of the section border, middle ones get the side border and the bottom one gets – well, the bottom part). So, if we just want a plain style, and we don’t have a custom backgroundView for our cells (which is the case in 90% of the times), then all we need to do is use UITableViewStyleGrouped, and remove the custom background. This can be done by following those two steps:
Change our tableView style to UITableViewStyleGrouped Add the following line to cellForRow, just before we return the cell:
cell.backgroundView=[[[UIView alloc] initWithFrame:cell.bounds] autorelease];
And that’s it. The tableView style will become exactly like UITableViewStylePlain, except for the floating headers.
Hope this helps!
Solution 14:[14]
To remove the floating section header sections completely, you can do this:
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
return [[UIView alloc] init];
}
return nil doesn't work.
To disable floating but still show section headers you can provide a custom view with its own behaviours.
Solution 15:[15]
Another way to do it is to make an empty section right before the one you want the header on and put your header on that section. Because the section is empty the header will scroll immediately.
Solution 16:[16]
You can add one Section(with zero rows) above, then set the above sectionFooterView as current section's headerView, footerView doesn't float. Hope it gives a help.
Solution 17:[17]
Ignore XAK. Do not explore any private methods if you want your app to have the chance of being accepted by apple.
This is easiest if you are using Interface Builder. You would add a UIView at the top of the view (where the images will go), then add your tableview below that. IB should size it accordingly; that is, the top of the tableview touches the bottom of the UIView you've just added and it's height covers the rest of the screen.
The thinking here is that if that UIView is not actually part of the table view, it will not scroll with the tableview. i.e. ignore the tableview header.
If you're not using interface builder, it's a little more complicated because you've got to get the positioning and height correct for the tableview.
Solution 18:[18]
Check the answer how to implement headers with StoryBoard: Table Header Views in StoryBoards
Also notice that if you don't implement
viewForHeaderInSection:(NSInteger)section
it will not float which is exactly what you want.
Solution 19:[19]
A variation on @samvermette's solution:
/// Allows for disabling scrolling headers in plain-styled tableviews
extension UITableView {
static let shouldScrollSectionHeadersDummyViewHeight = CGFloat(40)
var shouldScrollSectionHeaders: Bool {
set {
if newValue {
tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: bounds.size.width, height: UITableView.shouldScrollSectionHeadersDummyViewHeight))
contentInset = UIEdgeInsets(top: -UITableView.shouldScrollSectionHeadersDummyViewHeight, left: 0, bottom: 0, right: 0)
} else {
tableHeaderView = nil
contentInset = .zero
}
}
get {
return tableHeaderView != nil && contentInset.top == UITableView.shouldScrollSectionHeadersDummyViewHeight
}
}
}
Solution 20:[20]
The snippet display a sticky header only for the first section. Others section headers will float with a cells.
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
if section == 1 {
tableView.contentInset = .zero
}
}
func tableView(_ tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int) {
if section == 0 {
tableView.contentInset = .init(top: -tableView.sectionHeaderHeight, left: 0, bottom: 0, right: 0)
}
}
Solution 21:[21]
A tricky way is add an empty section for header. Because section has no cell, it will not floating at all.
Solution 22:[22]
**Swift 5.3 | Programmatically **
private func buildTableView() -> UITableView {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.rowHeight = UITableView.automaticDimension
tableView.showsVerticalScrollIndicator = false
tableView.separatorStyle = .none
let dummyViewHeight: CGFloat = 80
tableView.tableFooterView = UIView(
frame: CGRect(x: .zero,
y: .zero,
width: tableView.bounds.size.width,
height: dummyViewHeight))
tableView.contentInset = UIEdgeInsets(top: .zero, left: .zero, bottom: -dummyViewHeight, right: .zero)
return tableView
}
Solution 23:[23]
Best solution for below 2 scenarios:
Scenario 1: If the tableView style is not .grouped. Have no problem with changing it to grouped. footer wont float.
Scenario 2: If you want only one footer at the end of a tableView. and It's style is .plain
Steps:
- add one more section at the end.
- make sure in added section have (nuberOfRowsInSection method) zero number of rows.
- add custom footer to that section.
- set heightForFooterInSection to your custom footer and set .zero to other sections' footers.
example :
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
//note: Filter the section where you intended to add custom section
let footerView = UIView()
submitOrganiser?.showFooterAtTheEnd(view: footerView) //my method to customise the footer, use your implementation
return footerView
}
Solution 24:[24]
If you don't mind using a UICollectionView, this can also be achieved using UICollectionView list configurations in iOS 13.0+ and using headerMode set to .firstItemInSection.
override func viewDidLoad() {
super.viewDidLoad()
var config = UICollectionLayoutListConfiguration.init(appearance: .plain)
config.headerMode = .firstItemInSection
let listLayout = UICollectionViewCompositionalLayout.list(using: config)
self.collectionView.collectionViewLayout = listLayout
}
Solution 25:[25]
This can be achieved by assigning the header view manually in the UITableViewController's viewDidLoad method instead of using the delegate's viewForHeaderInSection and heightForHeaderInSection. For example in your subclass of UITableViewController, you can do something like this:
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *headerView = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, 40)];
[headerView setBackgroundColor:[UIColor magentaColor]];
[headerView setTextAlignment:NSTextAlignmentCenter];
[headerView setText:@"Hello World"];
[[self tableView] setTableHeaderView:headerView];
}
The header view will then disappear when the user scrolls. I don't know why this works like this, but it seems to achieve what you're looking to do.
Solution 26:[26]
Maybe you can simply make header view background transparent:
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section {
view.tintColor = [UIColor clearColor];
}
Or apply it globally:
[UITableViewHeaderFooterView appearance].tintColor = [UIColor clearColor];
Solution 27:[27]
I have another even simpler solution, to be used without autolayout and with everything done through the XIB :
1/ Put your header in the tableview by drag and dropping it directly on the tableview.
2/ In the Size Inspector of the newly made header, just change its autosizing : you should only leave the top, left and right anchors, plus the fill horizontally.

That should do the trick !
Solution 28:[28]
According to @samvermette's answer?I've implemented the above code in Swift to make it easy for coders to use swift.
let dummyViewHeight = CGFloat(40)
self.tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: self.tableView.bounds.size.width, height: dummyViewHeight))
self.tableView.contentInset = UIEdgeInsetsMake(-dummyViewHeight, 0, 0, 0)
Solution 29:[29]
ref: https://stackoverflow.com/a/26306212
let tableView = UITableView(frame: .zero, style: .grouped)
PLUSE
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if section == 0 {
return 40
}else{
tableView.sectionHeaderHeight = 0
}
return 0
}
This helped to use header space
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow


