I have a xib
file with a UITableView
for which I want to add a custom section header view using the delegate method tableView:viewForHeaderInSection:
. Is there any possibility to design it in Interface Builder
and then change some of it's subview's properties programmatically?
My UITableView
has more section headers so creating one UIView
in Interface Builder
and returning it doesn't work, because I'd have to duplicate it, but there isn't any good method of doing it. Archiving and unarchiving it doesn't work for UIImage
s so UIImageView
s would show up blank.
Also, I don't want to create them programmatically because they are too complex and the resulting code would be hard to read and maintain.
Edit 1: Here is my tableView:viewForHeaderInSection:
method:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if ([tableView.dataSource tableView:tableView numberOfRowsInSection:section] == 0) {
return nil;
}
CGSize headerSize = CGSizeMake(self.view.frame.size.width, 100);
/* wrapper */
UIView *wrapperView = [UIView viewWithSize:headerSize];
wrapperView.backgroundColor = [UIColor colorWithHexString:@"2670ce"];
/* title */
CGPoint titleMargin = CGPointMake(15, 8);
UILabel *titleLabel = [UILabel labelWithText:self.categoriesNames[section] andFrame:CGEasyRectMake(titleMargin, CGSizeMake(headerSize.width - titleMargin.x * 2, 20))];
titleLabel.textColor = [UIColor whiteColor];
titleLabel.font = [UIFont fontWithStyle:FontStyleRegular andSize:14];
[wrapperView addSubview:titleLabel];
/* body wrapper */
CGPoint bodyWrapperMargin = CGPointMake(10, 8);
CGPoint bodyWrapperViewOrigin = CGPointMake(bodyWrapperMargin.x, CGRectGetMaxY(titleLabel.frame) + bodyWrapperMargin.y);
CGSize bodyWrapperViewSize = CGSizeMake(headerSize.width - bodyWrapperMargin.x * 2, headerSize.height - bodyWrapperViewOrigin.y - bodyWrapperMargin.y);
UIView *bodyWrapperView = [UIView viewWithFrame:CGEasyRectMake(bodyWrapperViewOrigin, bodyWrapperViewSize)];
[wrapperView addSubview:bodyWrapperView];
/* image */
NSInteger imageSize = 56;
NSString *imageName = [self getCategoryResourceItem:section + 1][@"image"];
UIImageView *imageView = [UIImageView imageViewWithImage:[UIImage imageNamed:imageName] andFrame:CGEasyRectMake(CGPointZero, CGEqualSizeMake(imageSize))];
imageView.layer.masksToBounds = YES;
imageView.layer.cornerRadius = imageSize / 2;
[bodyWrapperView addSubview:imageView];
/* labels */
NSInteger labelsWidth = 60;
UILabel *firstLabel = [UILabel labelWithText:@"first" andFrame:CGRectMake(imageSize + bodyWrapperMargin.x, 0, labelsWidth, 16)];
[bodyWrapperView addSubview:firstLabel];
UILabel *secondLabel = [UILabel labelWithText:@"second" andFrame:CGRectMake(imageSize + bodyWrapperMargin.x, 20, labelsWidth, 16)];
[bodyWrapperView addSubview:secondLabel];
UILabel *thirdLabel = [UILabel labelWithText:@"third" andFrame:CGRectMake(imageSize + bodyWrapperMargin.x, 40, labelsWidth, 16)];
[bodyWrapperView addSubview:thirdLabel];
[@[ firstLabel, secondLabel, thirdLabel ] forEachView:^(UIView *view) {
UILabel *label = (UILabel *)view;
label.textColor = [UIColor whiteColor];
label.font = [UIFont fontWithStyle:FontStyleLight andSize:11];
}];
/* line */
UIView *lineView = [UIView viewWithFrame:CGRectMake(imageSize + labelsWidth + bodyWrapperMargin.x * 2, bodyWrapperMargin.y, 1, bodyWrapperView.frame.size.height - bodyWrapperMargin.y * 2)];
lineView.backgroundColor = [UIColor whiteColorWithAlpha:0.2];
[bodyWrapperView addSubview:lineView];
/* progress */
CGPoint progressSliderOrigin = CGPointMake(imageSize + labelsWidth + bodyWrapperMargin.x * 3 + 1, bodyWrapperView.frame.size.height / 2 - 15);
CGSize progressSliderSize = CGSizeMake(bodyWrapperViewSize.width - bodyWrapperMargin.x - progressSliderOrigin.x, 30);
UISlider *progressSlider = [UISlider viewWithFrame:CGEasyRectMake(progressSliderOrigin, progressSliderSize)];
progressSlider.value = [self getCategoryProgress];
[bodyWrapperView addSubview:progressSlider];
return wrapperView;
}
and I would want it to look something like this:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if ([tableView.dataSource tableView:tableView numberOfRowsInSection:section] == 0) {
return nil;
}
SectionView *sectionView = ... // get the view that is already designed in the Interface Builder
sectionView.headerText = self.categoriesNames[section];
sectionView.headerImage = [self getCategoryResourceItem:section + 1][@"image"];
sectionView.firstLabelText = @"first";
sectionView.secondLabelText = @"second";
sectionView.thirdLabelText = @"third";
sectionView.progress = [self getCategoryProgress];
return wrapperView;
}
Edit 2: I'm not using a Storyboard
, just .xib
files. Also, I don't have an UITableViewController
, just an UIViewController
in which I added an UITableView
.
#Storyboard or XIB. Updated for 2020.
Same Storyboard
:
return tableView.dequeueReusableCell(withIdentifier: "header")
Separate XIB
(Additional step: you must register that Nib
first):
tableView.register(UINib(nibName: "XIBSectionHeader", bundle:nil), forCellReuseIdentifier: "xibheader")
To load from a Storyboard
instead of a XIB
, see this Stack Overflow answer.
#Using UITableViewCell to create Section Header in IB
Take advantage of the fact that a section header is a regular UIView
, and that UITableViewCell
is, too, a UIView
. In Interface Builder, drag & drop a Table View Cell from the Object Library onto your Table View Prototype Content.
(2020) In modern Xcode, simply increase the "Dynamic Prototypes" number to drop in more cells:
Add an Identifier to the newly added Table View Cell, and customize its appearance to suit your needs. For this example, I used header
.
Use dequeueReusableCell:withIdentifier
to locate the cell, just like you would any table view cell.
Don't forget it is just a normal cell: but you are going to use it as a header.
For 2020, simply add to ViewDidLoad
the four lines of code:
tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 70 // any reasonable value is fine tableView.sectionHeaderHeight = UITableView.automaticDimension tableView.estimatedSectionHeaderHeight = 70 // any reasonable value is fine
{See for example this for a discussion.}
Your header cell heights are now completely dynamic. It's fine to change the length of the texts, etc, in the headers.
(TiP: Purely regarding the storyboard: simply select...
...in storyboard, so that the storyboard will work correctly. This has absolutely no effect on the final build. Selecting that checkbox has absolutely no effect whatsoever on the final build. It purely exists to make the storyboard work correctly, if the height is dynamic.)
In older Xcode, or, if for some reason you do not wish to use dynamic heights:
simply supply heightForHeaderInSection
, which is hardcoded as 44 for clarity in this example:
//MARK: UITableViewDelegate override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { // This is where you would change section header content return tableView.dequeueReusableCell(withIdentifier: "header") } override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 44 }
###Swift 2 & earlier:
return tableView.dequeueReusableCellWithIdentifier("header") as? UIView self.tableView.registerNib(UINib(nibName: "XIBSectionHeader", bundle:nil), forCellReuseIdentifier: "xibheader")
► Find this solution on GitHub and additional details on Swift Recipes.
I finally solved it using this tutorial, which, largely consists of the following (adapted to my example):
SectionHeaderView
class that subclasses UIView
.SectionHeaderView.xib
file and set it's File's Owner
's CustomClass
to the SectionHeaderView
class.UIView
property in the .m
file like: @property (strong, nonatomic) IBOutlet UIView *viewContent;
.xib
's View
to this viewContent
outlet.Add an initializer method that looks like this:
+ (instancetype)header {
SectionHeaderView *sectionHeaderView = [[SectionHeaderView alloc] init];
if (sectionHeaderView) { // important part
sectionHeaderView.viewContent = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:sectionHeaderView options:nil] firstObject];
[sectionHeaderView addSubview:sectionHeaderView.viewContent];
return sectionHeaderView;
}
return nil;
}
Then, I added an UILabel
inside the .xib
file and connected it to the labelCategoryName
outlet and implemented the setCategoryName:
method inside the SectionHeaderView
class like this:
- (void)setCategoryName:(NSString *)categoryName {
self.labelCategoryName.text = categoryName;
}
I then implemented the tableView:viewForHeaderInSection:
method like this:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
SectionHeaderView *sectionHeaderView = [SectionHeaderView header];
[sectionHeaderView setCategoryName:self.categoriesNames[section]];
return sectionHeaderView;
}
And it finally worked. Every section has it's own name, and also UIImageView
s show up properly.
Hope it helps others that stumble over the same wrong solutions over and over again, all over the web, like I did.
Solution Is way simple
Create one xib, make UI according to your Documentation then in viewForHeaderInSection get xib
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
NSArray *nibArray = [[NSBundle mainBundle] loadNibNamed:@"HeaderView" owner:self options:nil];
HeaderView *headerView = [nibArray objectAtIndex:0];
return headerView;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With