22

I have a UIView subclass that contains a multi-line UILabel. This view uses autolayout.

enter image description here

I would like to set this view as the tableHeaderView of a UITableView (not a section header). The height of this header will depend on the text of the label, which in turn depends on the width of the device. The sort of scenario autolayout should be great at.

I have found and attempted many many solutions to get this working, but to no avail. Some of the things I've tried:

  • setting a preferredMaxLayoutWidth on each label during layoutSubviews
  • defining an intrinsicContentSize
  • attempting to figure out the required size for the view and setting the tableHeaderView's frame manually.
  • adding a width constraint to the view when the header is set
  • a bunch of other things

Some of the various failures I've encountered:

  • label extends beyond the width of the view, doesn't wrap
  • frame's height is 0
  • app crashes with exception Auto Layout still required after executing -layoutSubviews

The solution (or solutions, if necessary) should work for both iOS 7 and iOS 8. Note that all of this is being done programmatically. I've set up a small sample project in case you want to hack on it to see the issue. I've reset my efforts to the following start point:

SCAMessageView *header = [[SCAMessageView alloc] init];
header.titleLabel.text = @"Warning";
header.subtitleLabel.text = @"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";
self.tableView.tableHeaderView = header;

What am I missing?

Community
  • 1
  • 1
Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • Im confused as to what is wrong with what you have now? The screenshot looks good... – Peter Foti Jan 22 '15 at 00:55
  • For the screenshot I manually set a frame on the header. – Ben Packard Jan 22 '15 at 00:56
  • @BenPackard I struggled this for a while as well and as best I can tell, it's not possible to make the `tableHeaderView` respect auto layout. Ultimately, I added an extra view inside the header view wrapping all the content. In `layoutSubviews` of the table view's superview I grabbed the size of the wrapper and manually set the frame of the `tableHeaderView`. – Anthony Mattox Jan 22 '15 at 01:35
  • @AnthonyMattox This seems less worse than my own solutions. I can't get it to work though. Could you provide more info please? Specifically, what are you doing in `layoutSubviews` to calculate the necessary size? – Ben Packard Jan 22 '15 at 02:39
  • 1
    @BenPackard I might be a little off. In the situation I had faced there was more custom layout going on within the table header view so it might have worked incidentally. I struggled a little further with coming up with a clean working example, but the `tableHeaderView` is extremely finicky. – Anthony Mattox Jan 26 '15 at 15:31
  • Possible duplicate of [Is it possible to use AutoLayout with UITableView's tableHeaderView?](http://stackoverflow.com/questions/16471846/is-it-possible-to-use-autolayout-with-uitableviews-tableheaderview) – ItsASecret Apr 26 '17 at 08:45

9 Answers9

27

My own best answer so far involves setting the tableHeaderView once and forcing a layout pass. This allows a required size to be measured, which I then use to set the frame of the header. And, as is common with tableHeaderViews, I have to again set it a second time to apply the change.

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.header = [[SCAMessageView alloc] init];
    self.header.titleLabel.text = @"Warning";
    self.header.subtitleLabel.text = @"This is a message with enough text to span multiple lines. This text is set at runtime and might be short or long.";

    //set the tableHeaderView so that the required height can be determined
    self.tableView.tableHeaderView = self.header;
    [self.header setNeedsLayout];
    [self.header layoutIfNeeded];
    CGFloat height = [self.header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    //update the header's frame and set it again
    CGRect headerFrame = self.header.frame;
    headerFrame.size.height = height;
    self.header.frame = headerFrame;
    self.tableView.tableHeaderView = self.header;
}

For multiline labels, this also relies on the custom view (the message view in this case) setting the preferredMaxLayoutWidth of each:

- (void)layoutSubviews
{
    [super layoutSubviews];

    self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame);
    self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame);
}

Update January 2015

Unfortunately this still seems necessary. Here is a swift version of the layout process:

tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
var frame = header.frame
frame.size.height = height
header.frame = frame
tableView.tableHeaderView = header

I've found it useful to move this into an extension on UITableView:

extension UITableView {
    //set the tableHeaderView so that the required height can be determined, update the header's frame and set it again
    func setAndLayoutTableHeaderView(header: UIView) {
        self.tableHeaderView = header
        header.setNeedsLayout()
        header.layoutIfNeeded()
        let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        var frame = header.frame
        frame.size.height = height
        header.frame = frame
        self.tableHeaderView = header
    }
}

Usage:

let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)
Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • This has got to be a bug in iOS?! Anyway, thanks for this, I'll have a go at implementing it. I've been tearing my hair out with many of the "solutions" you link to in your question. – Fogmeister Feb 17 '16 at 12:42
  • **SO ANNOYING!** I had to manually set the frame to twice the desired height to get a `tableFooterView` to not be zero height – race_carr Mar 28 '16 at 18:17
  • 1
    I can only get this to work if I add the code to `viewDidAppear`, but there is a quick flicker before it transitions to the new size. If I add the code to `viewDidLoad` it has no effect. Strangely, if I add it to `viewWillLayoutSubviews` it has no effect unless I rotate the display and then rotate it back. – Greg Ferreri Apr 22 '16 at 20:11
  • Try to make the following headerView: x-centered square UIView at 10 pt from the top, x-centered UILabel under UIView separated by 10 pt at 10 pt from bottom. (for example, the header of FB Messenger settings screen). This frame solution will not work for the case described. – malex Jul 15 '16 at 02:40
  • @Ben Packard, it seems, that this work around doesn't work on iOS 10 anymore. The final line ` self.tableHeaderView = header` sets the header with the correct frame. Despite this, inside `viewDidLayoutSubviews` frame is incorrect (`frame = (0 0; 375 0);`) – Daumantas Versockas Oct 24 '16 at 09:50
  • I needed to remove the first line (setting the header before sizing it), otherwise it would use the incorrect size. – Jay Peyer Feb 13 '17 at 18:42
19

For anyone still looking for a solution, this is for Swift 3 & iOS 9+. Here is one using only AutoLayout. It also updates correctly on device rotation.

extension UITableView {
    // 1.
    func setTableHeaderView(headerView: UIView) {
        headerView.translatesAutoresizingMaskIntoConstraints = false

        self.tableHeaderView = headerView

        // ** Must setup AutoLayout after set tableHeaderView.
        headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
        headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
        headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
    }

    // 2.
    func shouldUpdateHeaderViewFrame() -> Bool {
        guard let headerView = self.tableHeaderView else { return false }
        let oldSize = headerView.bounds.size        
        // Update the size
        headerView.layoutIfNeeded()
        let newSize = headerView.bounds.size
        return oldSize != newSize
    }
}

To use:

override func viewDidLoad() {
    ...

    // 1.
    self.tableView.setTableHeaderView(headerView: customView)
}

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    // 2. Reflect the latest size in tableHeaderView
    if self.tableView.shouldUpdateHeaderViewFrame() {

        // **This is where table view's content (tableHeaderView, section headers, cells) 
        // frames are updated to account for the new table header size.
        self.tableView.beginUpdates()
        self.tableView.endUpdates()
    }
}

The gist is that you should let tableView manage the frame of tableHeaderView the same way as table view cells. This is done through tableView's beginUpdates/endUpdates.

The thing is that tableView doesn't care about AutoLayout when it updates the children frames. It uses the current tableHeaderView's size to determine where the first cell/section header should be.

1) Add a width constraint so that the tableHeaderView uses this width whenever we call layoutIfNeeded(). Also add centerX and top constraints to position it correctly relative to the tableView.

2) To let the tableView knows about the latest size of tableHeaderView, e.g., when the device is rotated, in viewDidLayoutSubviews we can call layoutIfNeeded() on tableHeaderView. Then, if the size is changed, call beginUpdates/endUpdates.

Note that I don't include beginUpdates/endUpdates in one function, as we might want to defer the call to later.

Check out a sample project

aunnnn
  • 1,882
  • 2
  • 17
  • 23
  • It's working fine. Although I need to add height constraint headerView.heightAnchor.constraint(equalToConstant: 412).isActive = true – Sukhpreet Oct 24 '17 at 05:05
  • Your widthAnchor is probably redundant, from the [docs](https://developer.apple.com/documentation/uikit/uitableview/1614976-tablefooterview): "The table view respects only the height of your view's frame rectangle; it adjusts the width of your footer view automatically to match the table view's width." – Leon Apr 16 '20 at 15:21
3

The following UITableView extension solves all common problems of autolayouting and positioning of the tableHeaderView without frame-use legacy:

@implementation UITableView (AMHeaderView)

- (void)am_insertHeaderView:(UIView *)headerView
{
    self.tableHeaderView = headerView;

    NSLayoutConstraint *constraint = 
    [NSLayoutConstraint constraintWithItem: headerView
                                 attribute: NSLayoutAttributeWidth
                                 relatedBy: NSLayoutRelationEqual
                                    toItem: headerView.superview
                                 attribute: NSLayoutAttributeWidth
                                multiplier: 1.0
                                  constant: 0.0];
    [headerView.superview addConstraint:constraint];    
    [headerView layoutIfNeeded];

    NSArray *constraints = headerView.constraints;
    [headerView removeConstraints:constraints];

    UIView *layoutView = [UIView new];
    layoutView.translatesAutoresizingMaskIntoConstraints = NO;
    [headerView insertSubview:layoutView atIndex:0];

    [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];
    [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]];

    [headerView addConstraints:constraints];

    self.tableHeaderView = headerView;
    [headerView layoutIfNeeded];
}

@end

Explanation of the "strange" steps:

  1. At first we tie the headerView width to the tableView width: it helps as under rotations and prevent from deep left shift of X-centered subviews of the headerView.

  2. (the Magic!) We insert fake layoutView in the headerView: At this moment we STRONGLY need to remove all headerView constraints, expand the layoutView to the headerView and then restore initial headerView constraints. It happens that order of constraints has some sense! In the way we get correct headerView height auto calculation and also correct
    X-centralization for all headerView subviews.

  3. Then we only need to re-layout headerView again to obtain correct tableView
    height calculation and headerView positioning above sections without intersecting.

P.S. It works for iOS8 also. It is impossible to comment out any code string here in common case.

malex
  • 9,874
  • 3
  • 56
  • 77
  • This only solution that actually worked..... that was maddening. Thanks! – iwasrobbed Aug 04 '16 at 17:18
  • unfortunately this answer (like many answers after 10 years) is now totally wrong. the solution: https://stackoverflow.com/a/76540476/294884 – Fattie Jun 23 '23 at 13:15
2

Some of the answers here helped me get very close to what I needed. But I encountered conflicts with the constraint "UIView-Encapsulated-Layout-Width" which is set by the system, when rotating the device back-and-forth between portrait and landscape. My solution below is largely based on this gist by marcoarment (credit to him): https://gist.github.com/marcoarment/1105553afba6b4900c10. The solution does not rely on the header view containing a UILabel. There are 3 parts:

  1. A function defined in an extension to UITableView.
  2. Call the function from the view controller's viewWillAppear().
  3. Call the function from the view controller's viewWillTransition() in order to handle device rotation.

UITableView extension

func rr_layoutTableHeaderView(width:CGFloat) {
    // remove headerView from tableHeaderView:
    guard let headerView = self.tableHeaderView else { return }
    headerView.removeFromSuperview()
    self.tableHeaderView = nil

    // create new superview for headerView (so that autolayout can work):
    let temporaryContainer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    temporaryContainer.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(temporaryContainer)
    temporaryContainer.addSubview(headerView)

    // set width constraint on the headerView and calculate the right size (in particular the height):
    headerView.translatesAutoresizingMaskIntoConstraints = false
    let temporaryWidthConstraint = NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: width)
    temporaryWidthConstraint.priority = 999     // necessary to avoid conflict with "UIView-Encapsulated-Layout-Width"
    headerView.addConstraint(temporaryWidthConstraint)
    headerView.frame.size = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize)

    // remove the temporary constraint:
    headerView.removeConstraint(temporaryWidthConstraint)
    headerView.translatesAutoresizingMaskIntoConstraints = true

    // put the headerView back into the tableHeaderView:
    headerView.removeFromSuperview()
    temporaryContainer.removeFromSuperview()
    self.tableHeaderView = headerView
}

Use in UITableViewController

override func viewDidLoad() {
    super.viewDidLoad()

    // build the header view using autolayout:
    let button = UIButton()
    let label = UILabel()
    button.setTitle("Tap here", for: .normal)
    label.text = "The text in this header will span multiple lines if necessary"
    label.numberOfLines = 0
    let headerView = UIStackView(arrangedSubviews: [button, label])
    headerView.axis = .horizontal
    // assign the header view:
    self.tableView.tableHeaderView = headerView

    // continue with other things...
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.tableView.rr_layoutTableHeaderView(width: view.frame.width)
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    self.tableView.rr_layoutTableHeaderView(width: size.width)
}
rene
  • 1,975
  • 21
  • 33
2

This should do the trick for a headerView or a footerView for the UITableView using AutoLayout.

extension UITableView {

  var tableHeaderViewWithAutolayout: UIView? {
    set (view) {
      tableHeaderView = view
      if let view = view {
        lowerPriorities(view)
        view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        tableHeaderView = view
      }
    }
    get {
      return tableHeaderView
    }
  }

  var tableFooterViewWithAutolayout: UIView? {
    set (view) {
      tableFooterView = view
      if let view = view {
        lowerPriorities(view)
        view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
        tableFooterView = view
      }
    }
    get {
      return tableFooterView
    }
  }

  fileprivate func lowerPriorities(_ view: UIView) {
    for cons in view.constraints {
      if cons.priority.rawValue == 1000 {
        cons.priority = UILayoutPriority(rawValue: 999)
      }
      for v in view.subviews {
        lowerPriorities(v)
      }
    }
  }
}
Everton Cunha
  • 1,017
  • 8
  • 10
  • whilst I wouldn't code it like this, this answer provides **the critical insight, the vertical constraint chain must be <1000** bravo, bountied – Fattie Jun 27 '23 at 17:31
0

Using Extension in Swift 3.0

extension UITableView {

    func setTableHeaderView(headerView: UIView?) {
        // set the headerView
        tableHeaderView = headerView

        // check if the passed view is nil
        guard let headerView = headerView else { return }

        // check if the tableHeaderView superview view is nil just to avoid
        // to use the force unwrapping later. In case it fail something really
        // wrong happened
        guard let tableHeaderViewSuperview = tableHeaderView?.superview else {
            assertionFailure("This should not be reached!")
            return
        }

        // force updated layout
        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        // set tableHeaderView width
        tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0))

        // set tableHeaderView height
        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height))
    }

    func setTableFooterView(footerView: UIView?) {
        // set the footerView
        tableFooterView = footerView

        // check if the passed view is nil
        guard let footerView = footerView else { return }

        // check if the tableFooterView superview view is nil just to avoid
        // to use the force unwrapping later. In case it fail something really
        // wrong happened
        guard let tableFooterViewSuperview = tableFooterView?.superview else {
            assertionFailure("This should not be reached!")
            return
        }

        // force updated layout
        footerView.setNeedsLayout()
        footerView.layoutIfNeeded()

        // set tableFooterView width
        tableFooterViewSuperview.addConstraint(footerView.widthAnchor.constraint(equalTo: tableFooterViewSuperview.widthAnchor, multiplier: 1.0))

        // set tableFooterView height
        let height = footerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        tableFooterViewSuperview.addConstraint(footerView.heightAnchor.constraint(equalToConstant: height))
    }
}
Carmelo Gallo
  • 273
  • 4
  • 12
0

It is really this simple:

Preamble. Be aware that tableFooterView and tableHeaderView have absolutely no connection to involvement with the section footers/headers in a table view. They are not similar, related or associated in any way.

Furthermore, in UIKit the paradigm, the handling, and the view cycle treatment of "table" versus "section" decorations is utterly, utterly different and hence the whole issue is totally confusing. Add to this the totally bizarre issue explained here in point "2" (which is completely unlike any other constraint handling in all of UIKit) and one can see how mass confusion has arisen about table footers.

The overwhelming secret is this:

1. Your xib MUST HAVE <1000 vertical priority.

vert chain MUST be less than 1000

In your xib file your vertical constraint chain MUST be less than 1000. That's all there is to it. Load your xib file in the usual way.

class DemoFooter: UIIView {
    class func build() -> UIView {
        return UINib(nibName: "DemoFooter", bundle: nil)
         .instantiate(withOwner: nil, options: nil)[0] as! UIView
    }
}

That is the overwhelming "secret" to making table headers/footers work.

2. Due to an Apple issue, you MUST "reset" the tableFooterView. It's just how it is. It's completely pointless trying to work around this Apple issue.

You'd expect the code to look like this

tableFooterView.frame = .. the size

The simple fact is you cannot do that, you must do THIS:

temp = tableFooterView
temp.frame = .. the size
tableFooterView = temp

It's completely pointless discussing whether this is an Apple bug, soft bug, stupidity, bizarre behavior, mistake, or whatever. The fact is you have to "reset" the tableFooterView.

(In your code, simply try it also the other way, and you'll see that at first the footer just "floats on the middle of the screen". It makes absolutely no difference where you try it in the view cycle, waste time trying as you wish! It's just a typical UIKit f'up.)

3. Take care of incorrect view cycle example code seen on the internet.

override func viewDidLoad() {
    
    super.viewDidLoad()
    tableView.tableFooterView = DemoFooter.build()
}

override func viewWillLayoutSubviews() {
    
    if let f = tableView.tableFooterView {
        f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        tableView.tableFooterView = f
        print("Recall, you MUST HAVE <1000 priority vert in your xib.")
    }
    
    super.viewWillLayoutSubviews()
}

You do it in viewWillLayoutSubviews not later in the cycle.

You DO NOT "check if the height has changed" as seen in many examples on the net. This is obviously incredibly pointless / very poor practice for reasons not worth going in to here.

For clarity, if it's a trivial static view, you can just do this to avoid typing "viewDidLoad":

override func viewWillLayoutSubviews() {
    
    let f = QuickFoot.build()
    f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
    tableView.tableFooterView = f
    
    super.viewWillLayoutSubviews()
}

4. Double-check point (1).

Or, simply, nothing will work (and you'll get error spam in console).

Fattie
  • 27,874
  • 70
  • 431
  • 719
-3

Your constraints were just a little off. Take a look at this and let me know if you have any questions. For some reason I had difficulty getting the background of the view to stay red? So I created a filler view that fills the gap created by having a titleLabel and subtitleLabel height that is greater than the height of the imageView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.backgroundColor = [UIColor redColor];

        self.imageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"Exclamation"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
        self.imageView.tintColor = [UIColor whiteColor];
        self.imageView.translatesAutoresizingMaskIntoConstraints = NO;
        self.imageView.backgroundColor = [UIColor redColor];
        [self addSubview:self.imageView];
        [self.imageView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.equalTo(self);
            make.width.height.equalTo(@40);
            make.top.equalTo(self).offset(0);
        }];

        self.titleLabel = [[UILabel alloc] init];
        self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self.titleLabel.font = [UIFont systemFontOfSize:14];
        self.titleLabel.textColor = [UIColor whiteColor];
        self.titleLabel.backgroundColor = [UIColor redColor];
        [self addSubview:self.titleLabel];
        [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self).offset(0);
            make.left.equalTo(self.imageView.mas_right).offset(0);
            make.right.equalTo(self).offset(-10);
            make.height.equalTo(@15);
        }];

        self.subtitleLabel = [[UILabel alloc] init];
        self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self.subtitleLabel.font = [UIFont systemFontOfSize:13];
        self.subtitleLabel.textColor = [UIColor whiteColor];
        self.subtitleLabel.numberOfLines = 0;
        self.subtitleLabel.backgroundColor = [UIColor redColor];
        [self addSubview:self.subtitleLabel];
        [self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.titleLabel.mas_bottom);
            make.left.equalTo(self.imageView.mas_right);
            make.right.equalTo(self).offset(-10);
        }];

        UIView *fillerView = [[UIView alloc] init];
        fillerView.backgroundColor = [UIColor redColor];
        [self addSubview:fillerView];
        [fillerView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.equalTo(self.imageView.mas_bottom);
            make.bottom.equalTo(self.subtitleLabel.mas_bottom);
            make.left.equalTo(self);
            make.right.equalTo(self.subtitleLabel.mas_left);
        }];
    }

    return self;
}
Killian
  • 936
  • 3
  • 14
  • 28
  • Not sure if you misunderstood my question but I am not asking for help laying out the message view. Also, the reason you needed a filler view is because the message view itself has a height of 0. The labels are only visible because they are spilling outside the bounds. Set clipsToBounds = YES and you will see what I mean. If you were to install this view as a tableHeaderView, the table's cells would hide the labels since the header has a height of 0. – Ben Packard Jan 22 '15 at 03:08
-4

I'll add my 2 cents since this question is highly indexed in Google. I think you should be using

self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
self.tableView.estimatedSectionHeaderHeight = 200 //a rough estimate, doesn't need to be accurate

in your ViewDidLoad. Also, to load a custom UIView to a Header you should really be using viewForHeaderInSection delegate method. You can have a custom Nib file for your header (UIView nib). That Nib must have a controller class which subclasses UITableViewHeaderFooterView like-

class YourCustomHeader: UITableViewHeaderFooterView {
    //@IBOutlets, delegation and other methods as per your needs
}

Make sure your Nib file name is the same as the class name just so you don't get confused and it's easier to manage. like YourCustomHeader.xib and YourCustomHeader.swift (containing class YourCustomHeader). Then, just assign YourCustomHeader to your Nib file using identity inspector in the interface builder.

Then register the Nib file as your header view in the main View Controller's viewDidLoad like-

tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader")

And then in your heightForHeaderInSection just return UITableViewAutomaticDimension. This is how the delegates should look like-

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
     let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "YourCustomHeader") as! YourCustomHeader
     return headerView
}

func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
     return UITableViewAutomaticDimension
}

This is a much simpler and the appropriate way of doing without the "Hackish" ways suggested in the accepted answer since multiple forced layouts could impact your app's performance, especially if you have multiple custom headers in your tableview. Once you do the above method as I suggest, you would notice your Header (and or Footer) view expand and shrink magically based on your custom view's content size (provided you are using AutoLayout in the custom view, i.e. YourCustomHeader, nib file).

Anjan Biswas
  • 7,746
  • 5
  • 47
  • 77
  • 3
    This is a well written explanation of how to implement section headers but the question is about `tableHeaderView`. – Dylan Feb 22 '18 at 22:44