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
This question is related to
uitableview
ios5
uistoryboard
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;
}
I used to do the following to create header/footer views lazily:
[NSNull null]
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.
When you return cell's contentView you will have a 2 problems:
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;
}
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])
}
}
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
}
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
}
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:
UITableViewHeaderFooterView
viewDidLoad
viewForHeaderInSection
and use dequeueReusableHeaderFooterViewWithIdentifier
to get back the header/footerYou 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;
}
Add cell in StoryBoard
, and set reuseidentified
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
}
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.
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
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.
Just use a prototype cell as your section header and / or footer.
tableView:viewForHeaderInSection:
method or the tableView:viewForFooterInSection:
method[tableView dequeueReusableCellWithIdentifier:]
to get the headertableView: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):
tableView:viewForHeaderInSection:
method get the label by calling: UILabel *label = (UILabel *)[headerView viewWithTag:123];
[label setText:@"New Title"];
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
Source: Stackoverflow.com