[ios] 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,

This question is related to ios objective-c iphone cocoa-touch uikit

The answer is


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
}

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.

  1. 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;
    }
    
  2. 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;
    }
    
  3. Immplement numberOfSectionsInTableView as usual

    - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
    {
        int sectionCount = [sectionArray count];
        return sectionCount;
    }
    
  4. 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
    }
    
  5. Return 0.0f in heightForHeaderInSection.

    - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
    {
        return 0.0f;
    }
    
  6. DO NOT implement viewForHeaderInSection. Remove the method completely instead of returning nil.

  7. 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
        }
    }
    
  8. 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;
        }
    }
    
  9. 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;
    }
    
  10. 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.


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.


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.


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.


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.


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.


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.


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

}

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)

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

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;

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

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.


you can easily achieve this by implementing the viewForHeaderInSection method in the tableview delegate class. this method expects a UIView as return object (which is your header view). i have done this same thing in my code


2020 Latest Update

Tested with Xcode 14.

For hiding any section header, Return nil for title of section delegate

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return nil
}

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

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's how it should be set for the header

That should do the trick !


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


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!


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.


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.


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

In your Interface Builder click on your problem Table View

problem table view

Then navigate to Attributes Inspector and change Style Plain to Grouped ;) Easy

easy


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

A tricky way is add an empty section for header. Because section has no cell, it will not floating at all.


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

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.


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.


Examples related to ios

Adding a UISegmentedControl to UITableView Crop image to specified size and picture location Undefined Symbols error when integrating Apptentive iOS SDK via Cocoapods Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Autoresize View When SubViews are Added Warp \ bend effect on a UIView? Speech input for visually impaired users without the need to tap the screen make UITableViewCell selectable only while editing Xcode 12, building for iOS Simulator, but linking in object file built for iOS, for architecture arm64

Examples related to objective-c

Adding a UISegmentedControl to UITableView Keep placeholder text in UITextField on input in IOS Accessing AppDelegate from framework? Warp \ bend effect on a UIView? Use NSInteger as array index Detect if the device is iPhone X Linker Command failed with exit code 1 (use -v to see invocation), Xcode 8, Swift 3 ITSAppUsesNonExemptEncryption export compliance while internal testing? How to enable back/left swipe gesture in UINavigationController after setting leftBarButtonItem? Change status bar text color to light in iOS 9 with Objective-C

Examples related to iphone

Detect if the device is iPhone X Xcode 8 shows error that provisioning profile doesn't include signing certificate Access files in /var/mobile/Containers/Data/Application without jailbreaking iPhone Certificate has either expired or has been revoked Missing Compliance in Status when I add built for internal testing in Test Flight.How to solve? cordova run with ios error .. Error code 65 for command: xcodebuild with args: "Could not find Developer Disk Image" Reason: no suitable image found iPad Multitasking support requires these orientations How to insert new cell into UITableView in Swift

Examples related to cocoa-touch

Include of non-modular header inside framework module Move textfield when keyboard appears swift Create space at the beginning of a UITextField Navigation bar with UIImage for title Generate a UUID on iOS from Swift How do I write a custom init for a UIView subclass in Swift? creating custom tableview cells in swift How would I create a UIAlertView in Swift? Get current NSDate in timestamp format How do you add an in-app purchase to an iOS application?

Examples related to uikit

CGRectMake, CGPointMake, CGSizeMake, CGRectZero, CGPointZero is unavailable in Swift How to trap on UIViewAlertForUnsatisfiableConstraints? Changing Placeholder Text Color with Swift Move view with keyboard using Swift How to use UIVisualEffectView to Blur Image? preferredStatusBarStyle isn't called How to change Navigation Bar color in iOS 7? UICollectionView spacing margins How to programmatically get iOS status bar height Get UIScrollView to scroll to the top