152
votes

I have a UITableView with two sections. It is a simple table view. I am using viewForHeaderInSection to create custom views for these headers. So far, so good.

The default scrolling behavior is that when a section is encountered, the section header stays anchored below the Nav bar, until the next section scrolls into view.

My question is this: can I change the default behavior so that the section headers do NOT stay anchored at the top, but rather, scroll under the nav bar with the rest of the section rows?

Am I missing something obvious?

Thanks.

19
That must be one of the best written questions on this site! :) Thanks.Fattie
Check here for the right way to achieve this effect stackoverflow.com/a/13735238/1880899anemo

19 Answers

175
votes

The way I solved this problem is to adjust the contentOffset according to the contentInset in the UITableViewControllerDelegate (extends UIScrollViewDelegate) like this:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
       CGFloat sectionHeaderHeight = 40;
   if (scrollView.contentOffset.y<=sectionHeaderHeight&&scrollView.contentOffset.y>=0) {
       scrollView.contentInset = UIEdgeInsetsMake(-scrollView.contentOffset.y, 0, 0, 0);
   } else if (scrollView.contentOffset.y>=sectionHeaderHeight) {
       scrollView.contentInset = UIEdgeInsetsMake(-sectionHeaderHeight, 0, 0, 0);
   }
}

Only problem here is that it looses a little bit of bounce when scrolling back to the top.


{NOTE: The "40" should be the exact height of YOUR section 0 header. If you use a number that is bigger than your section 0 header height, you'll see that finger-feel is affected (try like "1000" and you'll see the bounce behaviour is sort of weird at the top). if the number matches your section 0 header height, finger feel seems to be either perfect or near-perfect.}

86
votes

You can also add a section with zero rows at the top and simply use the footer of the previous section as a header for the next.

36
votes

Were it me doing this, I'd take advantage of the fact that UITableViews in the Plain style have the sticky headers and ones in the Grouped style do not. I'd probably at least try using a custom table cell to mimic the appearance of Plain cells in a Grouped table.

I haven't actually tried this so it may not work, but that's what I'd suggest doing.

28
votes

I know it comes late, but I have found the definitive solution!

What you want to do is if you have 10 sections, let the dataSource return 20. Use even numbers for section headers, and odd numbers for section content. something like this

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (section%2 == 0) {
        return 0;
    }else {
        return 5;
    }
}

-(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    if (section%2 == 0) {
        return [NSString stringWithFormat:@"%i", section+1];
    }else {
        return nil;
    }
}

Voilá! :D

16
votes

Originally posted Here, a quick solution using the IB. The same can be done programmatically though quite simply.

A probably easier way to achieve this (using IB):

Drag a UIView onto your TableView to make it its header view.

  1. Set that header view height to 100px
  2. Set the tableview contentInset (top) to -100
  3. Section headers will now scroll just like any regular cell.

Some people commented saying that this solution hides the first header, however I have not noticed any such issue. It worked perfectly for me and was by far the simplest solution that I've seen so far.

15
votes

There are several things that need done to solve this problem in a non-hacky manner:

  1. Set the table view style to UITableViewStyleGrouped
  2. Set the table view backgroundColor to [UIColor clearColor]
  3. Set the backgroundView on each table view cell to an empty view with backgroundColor [UIColor clearColor]
  4. If necessary, set the table view rowHeight appropriately, or override tableView:heightForRowAtIndexPath: if individual rows have different heights.
15
votes

I was not happy with the solutions described here so far, so I tried to combine them. The result is the following code, inspired by @awulf and @cescofry. It works for me because I have no real table view header. If you already have a table view header, you may have to adjust the height.

// Set the edge inset
self.tableView.contentInset = UIEdgeInsetsMake(-23.0f, 0, 0, 0);

// Add a transparent UIView with the height of the section header (ARC enabled)
[self.tableView setTableHeaderView:[[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 23.0f)]];
14
votes

Just change TableView Style:

self.tableview = [[UITableView alloc] initwithFrame:frame style:UITableViewStyleGrouped];

UITableViewStyle Documentation:

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.

11
votes

Select Grouped TableView style

Select Grouped Table View style from your tableView's Attribute Inspector in storyboard.

5
votes

Set the headerView of the table with a transparent view with the same height of the header in section view. Also initi the tableview with a y frame at -height.

self.tableview = [[UITableView alloc] initWithFrame:CGRectMake(0, - height, 300, 400)];

UIView *headerView = [[[UIView alloc] initWithFrame:CGRectMake(0, 0, width, height)] autorelease];
[self.tableView setTableHeaderView:headerView];
4
votes

I found an alternative solution, use the first cell of each section instead a real header section, this solution don't appears so clean, but works so fine, you can use a defined prototype cell for your headers section, and in the method cellForRowAtIndexPath ask for the indexPath.row==0, if true, use the header section prototype cell, else use your default prototype cell.

4
votes

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. Hope this small change will help you ..

2
votes

Now that the grouped style looks basically the same as the plain style in iOS 7 (in terms of flatness and background), for us the best and easiest (i.e. least hacky) fix was to simply change the table view's style to grouped. Jacking with contentInsets was always a problem when we integrated a scroll-away nav bar at the top. With a grouped table view style, it looks exactly the same (with our cells) and the section headers stay fixed. No scrolling weirdness.

1
votes

Assign a negative inset to your tableView. If you have 22px high section headers, and you don't want them to be sticky, right after you reloadData add:

self.tableView.contentInset = UIEdgeInsetsMake(-22, 0, 0, 0); 
self.tableView.contentSize = CGSizeMake(self.tableView.contentSize.width, self.tableView.contentSize.height+22); 

Works like a charm for me. Works for section footers as well, just assign the negative inset on the bottom instead.

0
votes

I add the table to a Scroll View and that seems to work well.

0
votes

Check my answer here. This is the easiest way to implement the non-floating section headers without any hacks.

0
votes

@LocoMike's answer best fitted my tableView, however it broke when using footers as well. So, this is the corrected solution when using headers and footers:

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return (self.sections.count + 1) * 3;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    if (section % 3 != 1) {
        return 0;
    }
    section = section / 3;
    ...
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    if (section % 3 != 0) {
        return nil;
    }
    section = section / 3;
    ...
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
    if (section % 3 != 0) {
        return 0;
    }
    section = section / 3;
    ...
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
    if (section % 3 != 2) {
        return 0;
    }
    section = section / 3;
    ...
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    if (section % 3 != 0) {
        return nil;
    }
    section = section / 3;
    ...
}

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
    if (section % 3 != 2) {
        return nil;
    }
    section = section / 3;
    ...
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    int section = indexPath.section;
    section = section / 3;
    ...
}
0
votes

Swift version of @awulf answer, which works great!

func scrollViewDidScroll(scrollView: UIScrollView) {
    let sectionHeight: CGFloat = 80
    if scrollView.contentOffset.y <= sectionHeight {
        scrollView.contentInset = UIEdgeInsetsMake( -scrollView.contentOffset.y, 0, 0, 0)
    }else if scrollView.contentOffset.y >= sectionHeight {
        scrollView.contentInset = UIEdgeInsetsMake(-sectionHeight, 0, 0, 0)
    }
}
-2
votes

I've learned that just setting the tableHeaderView property does it, i.e. :

 tableView.tableHeaderView = customView;

and that's it.