b2cloud

29th November 2013

UITableView section header positions

Guides | Tutorial By 3 years ago

In one of my recent projects I added a content inset on the top of my table to push the start of the content down. Unfortuantely when you set the content inset the table assumes that all your sticky section headers now also start from the end of the content inset. This was not the behaviour I wanted, and instead I needed them to stick to the top of the table where the cells start to go out of bounds.

UIViewController* viewController = [[UIViewController alloc] init];
UINavigationController* navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];

UITableView* tableView = [[UITableView alloc] initWithFrame:viewController.view.bounds style:UITableViewStylePlain];
[tableView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)];
[tableView setDataSource:self];
[tableView setContentInset:UIEdgeInsetsMake(100, 0, 0, 0)];
[viewController.view addSubview:tableView];

UIView* headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[headerView setBackgroundColor:[UIColor redColor]];
[tableView setTableHeaderView:headerView];

[self.window setRootViewController:navigationController];

I made a UITableView subclass to do this, adding a headerViewInsets property that can be used to adjust where the sticky headers actually stick. It is probably a class that will help many others out.

UIViewController* viewController = [[UIViewController alloc] init];
UINavigationController* navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];

HeaderInsetTableView* tableView = [[HeaderInsetTableView alloc] initWithFrame:viewController.view.bounds style:UITableViewStylePlain];
[tableView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)];
[tableView setDataSource:self];
[tableView setContentInset:UIEdgeInsetsMake(100, 0, 0, 0)];
[tableView setHeaderViewInsets:UIEdgeInsetsMake(-100, 0, 0, 0)]; // Content inset's opposite for this example
[viewController.view addSubview:tableView];

UIView* headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
[headerView setBackgroundColor:[UIColor redColor]];
[tableView setTableHeaderView:headerView];

[self.window setRootViewController:navigationController];

Presto! Here’s the subclass:

HeaderInsetTableView.h

@interface HeaderInsetTableView : UITableView

//	To inset the header views
@property (nonatomic, assign) UIEdgeInsets headerViewInsets;

@end

HeaderInsetTableView.m

#import "HeaderInsetTableView.h"

@interface HeaderInsetTableView ()
{
	BOOL shouldManuallyLayoutHeaderViews;
}

- (void) layoutHeaderViews;

@end

@implementation HeaderInsetTableView

@synthesize headerViewInsets;

#pragma mark -
#pragma mark Super

- (void) layoutSubviews
{
	[super layoutSubviews];
	
	if(shouldManuallyLayoutHeaderViews)
		[self layoutHeaderViews];
}

#pragma mark -
#pragma mark Self

- (void) setHeaderViewInsets:(UIEdgeInsets)_headerViewInsets
{
	headerViewInsets = _headerViewInsets;
	
	shouldManuallyLayoutHeaderViews = !UIEdgeInsetsEqualToEdgeInsets(headerViewInsets, UIEdgeInsetsZero);
	
	[self setNeedsLayout];
}

#pragma mark -
#pragma mark Private

- (void) layoutHeaderViews
{
	const NSUInteger numberOfSections = self.numberOfSections;
	const UIEdgeInsets contentInset = self.contentInset;
	const CGPoint contentOffset = self.contentOffset;
	
	const CGFloat sectionViewMinimumOriginY = contentOffset.y + contentInset.top + headerViewInsets.top;
	
	//	Layout each header view
	for(NSUInteger section = 0; section < numberOfSections; section++)
	{
		UIView* sectionView = [self headerViewForSection:section];
		
		if(sectionView == nil)
			continue;
		
		const CGRect sectionFrame = [self rectForSection:section];
		
		CGRect sectionViewFrame = sectionView.frame;
		
		sectionViewFrame.origin.y = ((sectionFrame.origin.y < sectionViewMinimumOriginY) ? sectionViewMinimumOriginY : sectionFrame.origin.y);
		
		//	If it's not last section, manually 'stick' it to the below section if needed
		if(section < numberOfSections - 1)
		{
			const CGRect nextSectionFrame = [self rectForSection:section + 1];
			
			if(CGRectGetMaxY(sectionViewFrame) > CGRectGetMinY(nextSectionFrame))
				sectionViewFrame.origin.y = nextSectionFrame.origin.y - sectionViewFrame.size.height;
		}
		
		[sectionView setFrame:sectionViewFrame];
	}
}

@end
  • Hi, this works perfectly! Thanks! Any comments on how to make it work in a TableViewController? I was trying to get a TableViewController to use a subclass of TableView, but couldn’t do it. Any pointers would be of great help!

    • Tom

      The UITableViewController lets you set the UITableView, so you could subclass UITableViewController and set your custom tableView in the init or viewDidLoad methods.

  • Also, it doesn’t work when the Section Header is a custom view.

    I have a custom view created using the following instead of using the “titleForHeaderInSection” method —

    – (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 50;
    }

    – (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
    UIView* headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 50)];
    [headerView setBackgroundColor:[UIColor redColor]];
    return headerView;
    }

    The line “[self headerViewForSection:section];” in your code returns nil. Hence, not being able to do layout the header views.

    Any ideas for getting that to work?

    • It does work for custom header views. You are just not doing it properly.

      First of all create a class “myTableHeaderView” subclassing UITableViewHeaderFooterView and .nib file named appropriately. Add a UIView to the nib and set the class to “myTableHeaderView”.
      After that’s done you must register the .nib file to the UITableView with [self.tableView registerNib:[UINib nibWithNibName:@”myTableHeaderView” bundle:nil] forHeaderFooterViewReuseIdentifier:@”headerView”];. Then dequeue the headerView in – (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section method like this:

      -(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
      myTableHeaderView * headerView = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@”headerView”];
      return headerView;
      }

      This is the proper way to do it. Hope this helped you.

  • Alexander Frey

    Hi, I experience severe performance problems when scrolling the table view with my custom section headers (< 20fps). Can you confirm that you have native 50+fps ?

    • Tom

      Using my code as is you should get 60fps, anything other than that will probably be because of your headers/cells/drawing code
      Edit: try stripping back your custom code or using the time profiler to find which code is giving you a slow framerate

  • Lior Neu-ner

    Thanks for this, Tom! This really helped me out. Just an heads up, the images are broken on this page

Recommended Posts

Dynamic Headers in Xcode

Post by 3 years ago

There have been a few very rare scenarios where I have actually required a dynamic header in a project. I mention Xcode in the title however this technique really applies to nearly any IDE or...

Got an idea?

We help entrepreneurs, organizations and established brands from around
the country bring ideas to life. We would love to hear from you!