How to Implement Custom Table View Section Headers and Footers with Storyboard
UitableviewIos5UistoryboardUitableview Problem Overview
Without using a storyboard we could simply drag a UIView
onto the canvas, lay it out and then set it in the tableView:viewForHeaderInSection
or tableView:viewForFooterInSection
delegate methods.
How do we accomplish this with a StoryBoard where we cannot drag a UIView onto the canvas
Uitableview Solutions
Solution 1 - Uitableview
Just use a prototype cell as your section header and / or footer.
- add an extra cell and put your desired elements in it.
- set the identifier to something specific (in my case SectionHeader)
- implement the
tableView:viewForHeaderInSection:
method or thetableView:viewForFooterInSection:
method - use
[tableView dequeueReusableCellWithIdentifier:]
to get the header - implement the
tableView:heightForHeaderInSection:
method.
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = @"SectionHeader";
UITableViewCell *headerView = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (headerView == nil){
[NSException raise:@"headerView == nil.." format:@"No cells with matching CellIdentifier loaded from your storyboard"];
}
return headerView;
}
Edit: How to change the header title (commented question):
- Add a label to the header cell
- set the tag of the label to a specific number (e.g. 123)
- In your
tableView:viewForHeaderInSection:
method get the label by calling:
UILabel *label = (UILabel *)[headerView viewWithTag:123];
4. Now you can use the label to set a new title:
[label setText:@"New Title"];
Solution 2 - Uitableview
I know this question was for iOS 5, but for the benefit of future readers, note that effective iOS 6 we can now use dequeueReusableHeaderFooterViewWithIdentifier
instead of dequeueReusableCellWithIdentifier
.
So in viewDidLoad
, call either registerNib:forHeaderFooterViewReuseIdentifier:
or registerClass:forHeaderFooterViewReuseIdentifier:
. Then in viewForHeaderInSection
, call tableView:dequeueReusableHeaderFooterViewWithIdentifier:
. You do not use a cell prototype with this API (it's either a NIB-based view or a programmatically created view), but this is the new API for dequeued headers and footers.
Solution 3 - Uitableview
In iOS 6.0 and above, things have changed with the new dequeueReusableHeaderFooterViewWithIdentifier
API.
I have written a guide (tested on iOS 9), which can be summarised as such:
- Subclass
UITableViewHeaderFooterView
- Create Nib with the subclass view, and add 1 container view which contains all other views in the header/footer
- Register the Nib in
viewDidLoad
- Implement
viewForHeaderInSection
and usedequeueReusableHeaderFooterViewWithIdentifier
to get back the header/footer
Solution 4 - Uitableview
I got it working in iOS7 using a prototype cell in the storyboard. I have a button in my custom section header view that triggers a segue that is set up in the storyboard.
Start with Tieme's solution
As pedro.m points out, the problem with this is that tapping the section header causes the first cell in the section to be selected.
As Paul Von points out, this is fixed by returning the cell's contentView instead of the whole cell.
However, as Hons points out, a long press on said section header will crash the app.
The solution is to remove any gestureRecognizers from contentView.
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = @"SectionHeader";
UITableViewCell *sectionHeaderView = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
while (sectionHeaderView.contentView.gestureRecognizers.count) {
[sectionHeaderView.contentView removeGestureRecognizer:[sectionHeaderView.contentView.gestureRecognizers objectAtIndex:0]];
}
return sectionHeaderView.contentView; }
If you aren't using gestures in your section header views, this little hack seems to get it done.
Solution 5 - Uitableview
If you use storyboards you can use a prototype cell in the tableview to layout your header view. Set an unique id and viewForHeaderInSection you can dequeue the cell with that ID and cast it to a UIView.
Solution 6 - Uitableview
If you need a Swift Implementation of this follow the directions on the accepted answer and then in you UITableViewController implement the following methods:
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return tableView.dequeueReusableCell(withIdentifier: "CustomHeader")
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 75
}
Solution 7 - Uitableview
The solution I came up with is basically the same solution used before the introduction of storyboards.
Create a new, empty interface class file. Drag a UIView on to the canvas, layout as desired.
Load the nib manually, assign to the appropriate header/footer section in viewForHeaderInSection or viewForFooterInSection delegate methods.
I had hope that Apple simplified this scenario with storyboards and kept looking for a better or simpler solution. For example custom table headers and footers are straight forward to add.
Solution 8 - Uitableview
When you return cell's contentView you will have a 2 problems:
- crash related to gestures
- you don't reusing contentView (every time on
viewForHeaderInSection
call, you creating new cell)
Solution:
Wrapper class for table header\footer.
It is just container, inherited from UITableViewHeaderFooterView
, which holds cell inside
https://github.com/Magnat12/MGTableViewHeaderWrapperView.git
Register class in your UITableView (for example, in viewDidLoad)
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView registerClass:[MGTableViewHeaderWrapperView class] forHeaderFooterViewReuseIdentifier:@"ProfileEditSectionHeader"];
}
In your UITableViewDelegate:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
MGTableViewHeaderWrapperView *view = [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"ProfileEditSectionHeader"];
// init your custom cell
ProfileEditSectionTitleTableCell *cell = (ProfileEditSectionTitleTableCell * ) view.cell;
if (!cell) {
cell = [tableView dequeueReusableCellWithIdentifier:@"ProfileEditSectionTitleTableCell"];
view.cell = cell;
}
// Do something with your cell
return view;
}
Solution 9 - Uitableview
I've been in trouble within a scenario where Header was never reused even doing all the proper steps.
So as a tip note to everyone who want to achieve the situation of show empty sections (0 rows) be warn that:
dequeueReusableHeaderFooterViewWithIdentifier will not reuse the header until you return at least one row
Hope it helps
Solution 10 - Uitableview
I used to do the following to create header/footer views lazily:
- Add a freeform view controller for the section header/footer to the storyboard
- Handle all stuff for the header in the view controller
- In the table view controller provide a mutable array of view controllers for the section headers/footers repopulated with
[NSNull null]
- In viewForHeaderInSection/viewForFooterInSection if view controller does not yet exist, create it with storyboards instantiateViewControllerWithIdentifier, remember it in the array and return the view controllers view
Solution 11 - Uitableview
Similar to laszlo answer but you can reuse the same prototype cell for both the table cells and the section header cell. Add the first two functions below to your UIViewController subClass
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let cell = tableView.dequeueReusableCell(withIdentifier: "DataCell") as! DataCell
cell.data1Label.text = "DATA KEY"
cell.data2Label.text = "DATA VALUE"
return cell
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 75
}
// Example of regular data cell dataDelegate to round out the example
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DataCell", for: indexPath) as! PlayerCell
cell.data1Label.text = "\(dataList[indexPath.row].key)"
cell.data2Label.text = "\(dataList[indexPath.row].value)"
return cell
}
Solution 12 - Uitableview
To follow up on Damon's suggestion, here is how I made the header selectable just like a normal row with a disclosure indicator.
I added a Button subclassed from UIButton (subclass name "ButtonWithArgument") to the header's prototype cell and deleted the title text (the bold "Title" text is another UILabel in the prototype cell)
then set the Button to the entire header view, and added a disclosure indicator with Avario's trick
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
static NSString *CellIdentifier = @"PersonGroupHeader";
UITableViewCell *headerView = (UITableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(headerView == nil)
{
[NSException raise:@"headerView == nil, PersonGroupTableViewController" format:[NSString stringWithFormat:@"Storyboard does not have prototype cell with identifier %@",CellIdentifier]];
}
// https://stackoverflow.com/a/24044628/3075839
while(headerView.contentView.gestureRecognizers.count)
{
[headerView.contentView removeGestureRecognizer:[headerView.contentView.gestureRecognizers objectAtIndex:0]];
}
ButtonWithArgument *button = (ButtonWithArgument *)[headerView viewWithTag:4];
button.frame = headerView.bounds; // set tap area to entire header view
button.argument = [[NSNumber alloc] initWithInteger:section]; // from ButtonWithArguments subclass
[button addTarget:self action:@selector(headerViewTap:) forControlEvents:UIControlEventTouchUpInside];
// https://stackoverflow.com/a/20821178/3075839
UITableViewCell *disclosure = [[UITableViewCell alloc] init];
disclosure.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
disclosure.userInteractionEnabled = NO;
disclosure.frame = CGRectMake(button.bounds.origin.x + button.bounds.size.width - 20 - 5, // disclosure 20 px wide, right margin 5 px
(button.bounds.size.height - 20) / 2,
20,
20);
[button addSubview:disclosure];
// configure header title text
return headerView.contentView;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 35.0f;
}
-(void) headerViewTap:(UIGestureRecognizer *)gestureRecognizer;
{
NSLog(@"header tap");
NSInteger section = ((NSNumber *)sender.argument).integerValue;
// do something here
}
ButtonWithArgument.h
#import <UIKit/UIKit.h>
@interface ButtonWithArgument : UIButton
@property (nonatomic, strong) NSObject *argument;
@end
ButtonWithArgument.m
#import "ButtonWithArgument.h"
@implementation ButtonWithArgument
@end
Solution 13 - Uitableview
You should use Tieme's solution as a base but forget about the viewWithTag:
and other fishy approaches, instead try to reload your header (by reloading that section).
So after you sat up your custom cell-header view with all the fancy AutoLayout
stuff, just dequeue it and return the contentView after your set up, like:
-(UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
static NSString *CellIdentifier = @"SectionHeader";
SettingsTableViewCell *sectionHeaderCell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
sectionHeaderCell.myPrettyLabel.text = @"Greetings";
sectionHeaderCell.contentView.backgroundColor = [UIColor whiteColor]; // don't leave this transparent
return sectionHeaderCell.contentView;
}
Solution 14 - Uitableview
What about a solution where the header is based on a view array :
class myViewController: UIViewController {
var header: [UILabel] = myStringArray.map { (thisTitle: String) -> UILabel in
let headerView = UILabel()
headerView.text = thisTitle
return(headerView)
}
Next in the delegate :
extension myViewController: UITableViewDelegate {
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
return(header[section])
}
}
Solution 15 - Uitableview
-
Add cell in
StoryBoard
, and setreuseidentified
-
Code
class TP_TaskViewTableViewSectionHeader: UITableViewCell{ }
and
-
Use:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let header = tableView.dequeueReusableCell(withIdentifier: "header", for: IndexPath.init(row: 0, section: section)) return header }
Solution 16 - Uitableview
Here is @Vitaliy Gozhenko's answer, in Swift.
To summarize you will create a UITableViewHeaderFooterView that contains a UITableViewCell. This UITableViewCell will be "dequeuable" and you can design it in your storyboard.
-
Create a UITableViewHeaderFooterView class
class CustomHeaderFooterView: UITableViewHeaderFooterView { var cell : UITableViewCell? { willSet { cell?.removeFromSuperview() } didSet { if let cell = cell { cell.frame = self.bounds cell.autoresizingMask = [UIViewAutoresizing.FlexibleHeight, UIViewAutoresizing.FlexibleWidth] self.contentView.backgroundColor = UIColor .clearColor() self.contentView .addSubview(cell) } } }
-
Plug your tableview with this class in your viewDidLoad function:
self.tableView.registerClass(CustomHeaderFooterView.self, forHeaderFooterViewReuseIdentifier: "SECTION_ID")
-
When asking, for a section header, dequeue a CustomHeaderFooterView, and insert a cell into it
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let view = self.tableView.dequeueReusableHeaderFooterViewWithIdentifier("SECTION_ID") as! CustomHeaderFooterView if view.cell == nil { let cell = self.tableView.dequeueReusableCellWithIdentifier("Cell") view.cell = cell; } // Fill the cell with data here return view; }