42

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 UIImages so UIImageViews 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.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
  • 1
    You can create the UIView in Interface Builder give it to as section header – srinivas n Jul 29 '15 at 07:35
  • 1
    That's what I said, so, obviously, it doesn't just work, out of the box, hence, the question. How do I `give it` as a section header in a table with multiple sections? – Iulian Onofrei Jul 29 '15 at 07:37
  • 1
    Nope, the question was if I can design it in __INTERFACE BUILDER__, not in code! – Iulian Onofrei Jul 29 '15 at 07:42
  • 1
    Duplicating that `UIView` object. – Iulian Onofrei Jul 29 '15 at 07:48
  • 1
    What is your problem can you ex-plane clearly ,then i will try to help you – srinivas n Jul 29 '15 at 07:50
  • can you show me the code viewForHeaderInSection method – srinivas n Jul 29 '15 at 08:00
  • It doesn't matter, it's just some `UIView` elements creation code. – Iulian Onofrei Jul 29 '15 at 08:17
  • Duplicate http://stackoverflow.com/questions/9219234/how-to-implement-custom-table-view-section-headers-and-footers-with-storyboard – Vitalii Gozhenko Sep 01 '15 at 16:50
  • @VitaliyGozhenko, It's not, I use `.xib` files, not a Storyboard. Please remove your downvote. – Iulian Onofrei Sep 02 '15 at 07:35
  • A reason for the downvotes would help. Or is this just a case of the [Broken windows theory](https://en.wikipedia.org/wiki/Broken_windows_theory)?! -_- – Iulian Onofrei Sep 03 '15 at 07:33
  • @IulianOnofrei anyway duplicate of http://stackoverflow.com/questions/17651880/uitableviewheaderfooterview-in-interfacebuilder or http://stackoverflow.com/questions/19162246/uitableviewheaderfooterview-with-ib Please search before ask... – Vitalii Gozhenko Sep 03 '15 at 19:50
  • @VitaliyGozhenko, I did __effin__ search, but I couldn't find! I was only finding solutions like the ones above __which doesn't apply__. I searched for a solution for days. I'm not a retard. That's why the duplicate question exists. SO is going down, and also down on my nerves. It's not about helping anymore, it's about pointing fingers, downvoting, copy+paste-ing the same __wrong__ solution for a couple more reputation, and in the end, __hate__. – Iulian Onofrei Sep 04 '15 at 07:17

4 Answers4

100

#Storyboard or XIB. Updated for 2020.

  1. Same Storyboard:

     return tableView.dequeueReusableCell(withIdentifier: "header")
    

    Two Section Headers

  2. 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:

enter image description here

Add an Identifier to the newly added Table View Cell, and customize its appearance to suit your needs. For this example, I used header.

Edit the cell

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...

enter image description here

...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.

Fattie
  • 27,874
  • 70
  • 431
  • 719
SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • Sorry, I forgot to mention that I don't use a `Storyboard`, just a `.xib` file, in which I have a `UITableView`, not a `UITableViewController`. – Iulian Onofrei Aug 28 '15 at 07:08
  • Also, if I have an `UILabel` inside that `UITableViewCell`, how can I change it's text differently for every section? – Iulian Onofrei Aug 28 '15 at 07:19
  • So, if the section header `UITableViewCell` is in another `XIB` file, on which `UITableView` do I call the method `dequeueReusableCellWithIdentifier:`? – Iulian Onofrei Aug 31 '15 at 07:52
  • 2
    It is as simple as adding `self.tableView.registerNib(UINib(nibName: "XIBSectionHeader", bundle:nil), forCellReuseIdentifier: "xibheader")` in `UITableViewController -viewDidLoad()`. – SwiftArchitect Sep 01 '15 at 01:48
  • I don't have an `UITableViewController`. – Iulian Onofrei Sep 01 '15 at 07:32
  • 3
    Before use this way, check this answer. Because you will have a problem with layout/touch handling, and reusing of such section views. http://stackoverflow.com/questions/9219234/how-to-implement-custom-table-view-section-headers-and-footers-with-storyboard – Vitalii Gozhenko Sep 01 '15 at 16:49
  • Clarification: There is no mention of a `UITableViewController`, only a `UITableViewDelegate`. – SwiftArchitect Sep 01 '15 at 18:20
  • 5
    I think this has changed now. Use tableView.dequeueReusableCellWithIdentifier("header")?.contentView. Casting to UIView will always fail. – Joel Teply Jul 25 '16 at 21:01
  • 8
    @SwiftArchitect Any reason for not using the appropriate method? `dequeueReusableHeaderFooterView()` should be used instead of `dequeueReusableCellWithIdentifier`. – Frederik Winkelsdorf Dec 27 '16 at 16:22
  • `dequeueReusableHeaderFooterView` do not mix well with Storyboard. – SwiftArchitect Dec 30 '16 at 05:42
  • what does "same storyboard" mean?! – mfaani Jan 09 '17 at 22:03
  • The `UITableViewCell` is defined in the same Storyboard as the `UITableViewController`. – SwiftArchitect Jan 10 '17 at 07:13
  • 6
    "Not mixing well with storyboard" is a poor excuse for using a completely inappropriate method for your header and footer views. `dequeueReusableHeaderFooterView()` should definitely be used instead; @FrederikA.Winkelsdorf is correct – sethfri May 08 '17 at 07:10
  • Apart from @FrederikA.Winkelsdorf 's suggestion, this answer is spot on, This should be accepted – Aju Antony Jun 20 '17 at 04:58
  • Some of the comments here somewhat confuse the issue that this is the perfect approach in modern iOS, thanks again! – Fattie Jun 11 '20 at 22:50
  • @sethfri - your comment is incorrect: it *is not* a header or footer - it's a cell. **It is perfectly, totally, completely OK to use "table view cells" for *other purposes* - just for example, for some reason you could just use one "in a screen somewhere".** (We do that all the time to match a cell.) In this case you are (correctly) using a cell ... for header use. You *would not* use dequeueReusableHeaderFooterView ... because it's not a header! it's "just a view". It's completely OK to use a table cell as "just a view" if you need to for some reason, and that's what's happening here. – Fattie Jun 19 '20 at 17:24
  • @FrederikA.Winkelsdorf - that method would be quite wrong, it's not a header. It's just a normal cell. It's perfectly normal and OK to use a table cell as "just a view" (for one reason or another) - and that's what's happening here. – Fattie Jun 19 '20 at 17:25
  • 1
    @Fattie Well, I don't agree with you when you say "quite wrong". Apple still recommends it: "Always use a UITableViewHeaderFooterView". See the example for `viewForHeaderInSection` in the Docs on https://developer.apple.com/documentation/uikit/views_and_controls/table_views/adding_headers_and_footers_to_table_sections. That said of course it works well to use a Cell as a Header/Footer View without any issues I'm aware of. That's why my reply in 2016 was a question "Any reason for not using the _appropriate_ method?". If you are aware of any reason, feel free to shed some light on this. – Frederik Winkelsdorf Jun 20 '20 at 07:45
  • hi @FrederikA.Winkelsdorf , I really understand what you're saying but. Your comment was on the **method** used - dequeueReusableCell:withIdentifier. Note that in the approach presented in this answer, the "whole point of the answer" is that you are *using a normal cell*. So you **definitely** have to use the method "dequeueReusableCell:withIdentifier". *Given that* you are using a *normal cell* you simply must use that call. Indeed no other call will work, end of story. – Fattie Jun 20 '20 at 16:23
  • 1
    @FrederikA.Winkelsdorf , I really understand, your point may be: *"This whole answer is crap! Don't try to use a normal cell as a header!"* :) if so, that is a perfectly reasonable viewpoint! But it's confusing to say it is the wrong *method*. You simply have to use that method when using a cell (for any reason). – Fattie Jun 20 '20 at 16:25
  • @Fattie It is incorrect to dequeue a cell from the cell reuse queue when not using it to populate a cell in the table view. You can refer to the docs on this. If you want to use a cell for your header view for whatever reason, just instantiate one directly in `tableView(_:viewForHeaderInSection:)`. If you pull out a cell from the reuse queue and use it as a header, the data source can't mark it for reuse when the user scrolls, so you'll risk exhausting the reuse queue with each header. – sethfri Jun 20 '20 at 21:28
  • @Fattie Agreed, that was probably the part of the misunderstanding. I now got your point, thanks for the clarification. You focussed on me saying "method", where I should've said method *and* class/implementation that's dequeued. That went hand in hand in my question about not using the "appropriate method". Being more concise is never a bad idea when trying to help others. – Frederik Winkelsdorf Jun 21 '20 at 09:26
  • 1
    @FrederikA.Winkelsdorf ahhh, I see what you mean, the "method used", the ":technique" !! :) I particularly thought you literally meant the "method" ie that call :) :) cheers .. – Fattie Jun 21 '20 at 11:44
  • @Fattie Exactly, that was my (language) fault, next time I'll be more specific as this might really cause some confusion (as it did) ^^ :) Cheers – Frederik Winkelsdorf Jun 21 '20 at 12:39
  • I tentatively would agree with Frederik Winkelsdorf and caution against this technique because it does not subclass `UITableViewHeaderFooterView` as Apple says you should. In my current project I get jerky animations when inserting and deleting rows unless it is so. I suspect there are some hidden calculations done behind the scenes for this subclass. Need to investigate further but just a heads up if you run into this issue. – MH175 Jun 14 '21 at 22:08
12

I finally solved it using this tutorial, which, largely consists of the following (adapted to my example):

  1. Create SectionHeaderView class that subclasses UIView.
  2. Create SectionHeaderView.xib file and set it's File's Owner's CustomClass to the SectionHeaderView class.
  3. Create an UIView property in the .m file like: @property (strong, nonatomic) IBOutlet UIView *viewContent;
  4. Connect the .xib's View to this viewContent outlet.
  5. 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 UIImageViews 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.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
  • 1
    1) "All sorts"?! There's just 2 lines of code that does the job. 2) Why is a static `+` wrong? It's just a wrapper for `... alloc] init]`, I use it all the time like these custom initializers: `[UIView viewWithFrame:]`, `[UIView viewAtPoint:].` 3) What has loading an `UITableView` from a `XIB` has to do with my problem? I have a `XIB` file with __and__ `UITableView` and other `UIView`s, not only the table, and now, another `XIB` for the section header. 3) It's my first bounty, and nowhere was written that I'm not allowed to post a solution before the bounty expires. Why not let them vote mine? – Iulian Onofrei Sep 01 '15 at 07:30
  • 1
    Mate - Apple has such a lovely document covering headerfooter views. You can easily refer that for custom header footer view :) https://developer.apple.com/documentation/uikit/views_and_controls/table_views/adding_headers_and_footers_to_table_sections – SeriousSam Jun 15 '20 at 15:09
  • 1
    No, it doesn't. The example on that page is created programatically, which I stated that I don't want. – Iulian Onofrei Jun 15 '20 at 15:24
2

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;
} 
kalpesh
  • 1,285
  • 1
  • 17
  • 30
Bevan
  • 342
  • 2
  • 14
0

As far as I understand your problem, you want to have the same UIView duplicated multiple times for the multiple section headers you want to be able to display.

If this were my problem, here is how I would solve it.

ORIGINAL SOLUTION

1)

In my UIViewController that owns the table view, I'd also create a view that's a template for the header. Assign that to a IBOutlet. This will be the view you can edit via Interface Builder.

2)

In your ViewDidLoad or (maybe better) ViewWillAppear method, you'll want to make as many copies of that header template UIView as you'll need to display for section headers.

Making copies of UIViews in memory isn't trivial, but it isn't hard either. Here is an answer from a related question that shows you how to do it.

Add the copies to a NSMutableArray (where the index of each object will correspond to the sections... the view in index 0 of the array will be what you return for section 0, view 1 in the array for section 1, ec.).

3)

You will not be able to use IBOutlets for the elements of that section header (because your code only associates outlets with one particular view from the XIB file).

So for this, you'll probably want to use view tag properties for each of the UI elements in your header view that you'll want to modify/change for each different section. You can set these tags via Interface Builder and then refer to them programmatically in your code.

In your viewForHeaderInSection method, you'll do something like:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

    if ([tableView.dataSource tableView:tableView numberOfRowsInSection:section] == 0) {
        return nil;
    }

    SectionView *sectionView = [self.arrayOfDuplicatedHeaderViews objectAtIndex: section];
    // my title view has a tag of 10
    UILabel *titleToModify = [sectionView viewWithTag: 10]; 
    if(titleToModify)
    {
        titleToModify.text = [NSString stringWithFormat:@"section %d", section];
    }

    return sectionView;
}

Makes sense?

DIFFERENT SOLUTION

1)

You'd still need an array of UIViews (or "Section View" subclassed UIViews) but you could create each of those with successive calls to load the view from it's own XIB file.

Something like this:

@implementation SectionView

+ (SectionView*) getSectionView
{
  NSArray* array = [[NSBundle mainBundle] loadNibNamed:@"SectionView" owner:nil options:nil];
  return [array objectAtIndex:0]; // assume that SectionView is the only object in the xib
}

@end

(more detail found in the answer to this related question)

2)

You might be able to use IBOutlets on this (but I'm not 100% certain), but tag properties once again might work pretty well.

Community
  • 1
  • 1
Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
  • At the second point of your answer, there is a problem. As I stated out in the question: _"I'd have to duplicate it, but there isn't any good method of doing it. Archiving and unarchiving doesn't work for `UIImage`s so `UIImageView`s would show up blank."_. Second, working with tags I guess it's not quite a good practice, and I know this from my own experience where I find it hard to maintain a `UIViewController` from a project I'm working on it that uses tags. Thought it might work, I am curious if there isn't any simpler solution. Also, why is the `sectionView` surrounded by parens? – Iulian Onofrei Aug 27 '15 at 10:00
  • I did an update to my answer up there to offer a potentially more useful solution and yeah, I agree that tag properties aren't very friendly to use. I haven't done the "`loadNibNamed:`" method in my own code in a long time so I'm not certain if `IBOutlets` would work or not, but give them a try. And the parenths around the return value is just my style, it's not a requirement at all. I'll edit them out. – Michael Dautermann Aug 27 '15 at 10:15
  • How is this `loadNibNamed` solution proposed on Aug 27 any different from the accepted solution? – SwiftArchitect Sep 01 '15 at 18:18
  • probably because I made this solution first, @SwiftArchitect – Michael Dautermann Sep 01 '15 at 21:23