114

Since I discovered AutoLayout I use it everywhere, now I'm trying to use it with a tableHeaderView.

I made a subclass of UIView added everything (labels etc...) I wanted with their constraints, then I added this CustomView to the UITableView'tableHeaderView.

Everything works just fine except the UITableView always displays above the CustomView, by above I mean the CustomView is under the UITableView so it can't be seen !

It seems that no matter what I do, the height of the UITableView'tableHeaderView is always 0 (so is the width, x and y).

My question : is it possible at all to accomplish this without setting the frame manually ?

EDIT : The CustomView'subview that I'm using has these constraints :

_title = [[UILabel alloc]init];
_title.text = @"Title";
[self addSubview:_title];
[_title keep:[KeepTopInset rules:@[[KeepEqual must:5]]]]; // title has to stay at least 5 away from the supperview Top
[_title keep:[KeepRightInset rules:@[[KeepMin must:5]]]];
[_title keep:[KeepLeftInset rules:@[[KeepMin must:5]]]];
[_title keep:[KeepBottomInset rules:@[[KeepMin must:5]]]];

I'm using a handy library 'KeepLayout' because writing constraints manually takes forever and way too many line for one single constraint but the methods are self-explaining.

And the UITableView has these constraints :

_tableView = [[UITableView alloc]init];
_tableView.translatesAutoresizingMaskIntoConstraints = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_tableView];
[_tableView keep:[KeepTopInset rules:@[[KeepEqual must:0]]]];// These 4 constraints make the UITableView stays 0 away from the superview top left right and bottom.
[_tableView keep:[KeepLeftInset rules:@[[KeepEqual must:0]]]];
[_tableView keep:[KeepRightInset rules:@[[KeepEqual must:0]]]];
[_tableView keep:[KeepBottomInset rules:@[[KeepEqual must:0]]]];

_detailsView = [[CustomView alloc]init];
_tableView.tableHeaderView = _detailsView;

I don't know if I have to set some constraints directly on the CustomView, I think the height of the CustomView is determined by the constraints on the UILabel "title" in it.

EDIT 2: After another investigation it seems the height and width of the CustomView are correctly calculated, but the top of the CustomView is still at the same level than the top of the UITableView and they move together when I scroll.

Mo Abdul-Hameed
  • 6,030
  • 2
  • 23
  • 36
ItsASecret
  • 2,589
  • 3
  • 19
  • 32
  • Yes, it is possible. Can you show the code you're using? It's difficult to advise without knowing what constraints you have set up on the header view. – jrturton May 09 '13 at 22:14
  • An easy way for you to accomplish this is to add that view in **IB** to the tableView..just create the view in the same scene containing the tableview and **drag** it to the table. – Mariam K. May 09 '13 at 22:15
  • I'm trying to avoid IB the most I can, so far I didn't have to use it, if I can't get it to work I'll try with IB – ItsASecret May 09 '13 at 22:18
  • 1
    Apple advises developers to use IB whenever possible when it comes to autolayout. It really helps in avoiding a lot of inconsistency problems. – Mariam K. May 09 '13 at 22:20
  • The true complete autolayout solution is [here](http://stackoverflow.com/a/38386985/2066428) – malex Jul 15 '16 at 02:31
  • This answer solves this very eloquently. https://stackoverflow.com/questions/34661793/setting-tableheaderview-height-dynamically – Elijah Jul 06 '18 at 18:51
  • https://medium.com/@aunnnn/table-header-view-with-autolayout-13de4cfc4343 This solves this issue almost perfectly. – Schemetrical May 28 '19 at 22:41
  • **2023:** https://stackoverflow.com/a/76540476/294884 – Fattie Jun 23 '23 at 13:16

29 Answers29

153

I asked and answered a similar question here. In summary, I add the header once and use it to find the required height. That height can then be applied to the header, and the header is set a second time to reflect 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;
}

If you have multi-line labels, this also relies on the custom view setting the preferredMaxLayoutWidth of each label:

- (void)layoutSubviews
{
    [super layoutSubviews];

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

or perhaps more generally:

override func layoutSubviews() {
    super.layoutSubviews()  
    for view in subviews {
        guard let label = view as? UILabel where label.numberOfLines == 0 else { continue }
        label.preferredMaxLayoutWidth = CGRectGetWidth(label.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()
header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
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
        self.tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            header.widthAnchor.constraint(equalTo: self.widthAnchor)
        ])
        header.setNeedsLayout()
        header.layoutIfNeeded()
        header.frame.size =  header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        self.tableHeaderView = header
    }
}

Usage:

let header = SCAMessageView()
header.titleLabel.text = "Warning"
header.subtitleLabel.text = "Warning message here."
tableView.setAndLayoutTableHeaderView(header)
Will
  • 4,942
  • 2
  • 22
  • 47
Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • I can't really test this now since it's been a year and a half and 2 major iOS have been released since, but if this works then this seems to be what I wanted to do! – ItsASecret Jan 23 '15 at 20:09
  • Yeah, just figured I should add it for posterity. – Ben Packard Jan 23 '15 at 21:00
  • Worked for me after I removed my header from storyboard (from table view) and placed it into separate xib – Nik Apr 22 '15 at 21:56
  • 8
    An alternative to using `preferredMaxLayoutWidth` is adding a width constraint (equal to the table view's width) on the header view prior to using `systemLayoutSizeFittingSize:`. – Benjohn Jul 28 '15 at 12:55
  • 2
    NOTE: if you experiencing that the header is above the first cells, then you forgot to reset the header property to `self.tableView.tableHeaderView` – Laszlo Nov 06 '15 at 09:43
  • 29
    Often it amazes me how tricky it can be to do something completely trivial like this. – TylerJames Dec 04 '15 at 18:04
  • 2
    Jesus! label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame) is very important! Thanks – HotJard Aug 25 '16 at 12:21
  • 6
    NOTE: If you need to get exact width as tableView, you should get height with required horizontal priority `let height = header.systemLayoutSizeFittingSize(CGSizeMake(CGRectGetWidth(self.bounds), 0), withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityFittingSizeLevel).height` – JakubKnejzlik Sep 07 '16 at 08:31
  • @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 the `viewDidLayoutSubviews` frame is incorrect (`frame = (0 0; 375 0);`) – Daumantas Versockas Oct 24 '16 at 10:02
  • 4
    Just Use `header.setNeedsLayout() header.layoutIfNeeded() header.frame.size = header.systemLayoutSizeFitting(UILayoutFittingCompressedSize) self.tableHeaderView = header` would work at iOS 10.2 – Kesong Xie Feb 11 '17 at 05:29
  • 1
    Plus for adding multi-line label handle cases. – tounaobun Jun 26 '19 at 02:33
26

I've been unable to add a header view using constraints (in code). If I give my view a width and/or a height constraint, I get a crash with the message saying:

 "terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super."

When I add a view in the storyboard to my table view, it shows no constraints, and it works fine as a header view, so I think that the placement of the header view isn't done using constraints. It doesn't seem to behave like a normal view in that regard.

The width is automatically the width of the table view, the only thing you need to set is the height -- the origin values are ignored, so it doesn't matter what you put in for those. For instance, this worked fine (as does 0,0,0,80 for the rect):

UIView *headerview = [[UIView alloc] initWithFrame:CGRectMake(1000,1000, 0, 80)];
headerview.backgroundColor = [UIColor yellowColor];
self.tableView.tableHeaderView = headerview;
Kampai
  • 22,848
  • 21
  • 95
  • 95
rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • I had that exception too but adding a category to UITableView fixed it I found it in that answer : http://stackoverflow.com/questions/12610783/auto-layout-still-required-after-executing-layoutsubviews-with-uitableviewcel/12941256#12941256 – ItsASecret May 09 '13 at 23:33
  • I'm still gonna try what you suggest, but tomorrow morning, it's 1:34 am I'm going to bed, thank you very much for taking the time to answer ! (But I really want to not specify a height, I would like it to be calculated by the constraints I set up on the label in the CustomView) – ItsASecret May 09 '13 at 23:34
  • I've tried it and yeah setting the frame works, but I was looking for a way to avoid setting the frame, I'll keep looking and if I find nothing else I'll accept your answer – ItsASecret May 10 '13 at 09:47
  • 1
    I get this exception (currently testing 7.1) if the added header view has `translatesAutoresizingMaskIntoConstraints = NO`. Turning translation on prevents the error – I suspect `UITableView` as of 7.1 doesn't attempt to autolayout its header view and wants something with the frame pre-set. – Benjohn Jul 28 '15 at 12:53
19

I saw a lot of methods here doing so much unnecessary stuff, but you don't need that much to use auto layout in the header view. You just have to create you xib file, put your constraints and instantiate it like this:

func loadHeaderView () {
        guard let headerView = Bundle.main.loadNibNamed("CourseSearchHeader", owner: self, options: nil)?[0] as? UIView else {
            return
        }
        headerView.autoresizingMask = .flexibleWidth
        headerView.translatesAutoresizingMaskIntoConstraints = true
        tableView.tableHeaderView = headerView
    }
Ramon Vasconcelos
  • 1,466
  • 1
  • 21
  • 28
7

Another solution is to dispatch the header view creation to the next main thread call:

- (void)viewDidLoad {
    [super viewDidLoad];

    // ....

    dispatch_async(dispatch_get_main_queue(), ^{
        _profileView = [[MyView alloc] initWithNib:@"MyView.xib"];
        self.tableView.tableHeaderView = self.profileView;
    });
}

Note: It fix the bug when the loaded view has a fixed height. I haven't tried when the header height only depends on its content.

EDIT :

You can find a cleaner solution to this problem by implementing this function, and calling it in viewDidLayoutSubviews

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    [self sizeHeaderToFit];
}
Community
  • 1
  • 1
Martin
  • 11,881
  • 6
  • 64
  • 110
  • 1
    @TussLaszlo `tableHeaderView` are kind of buggy with autolayout. There is some workarounds, like this one. But since I wrote this, i've found a better and cleaner solution here http://stackoverflow.com/a/21099430/127493 by calling its `- (void)sizeHeaderToFit` in `viewDidLayoutSubviews ` – Martin Nov 06 '15 at 10:09
5

You can get autolayout to provide you with a size by using the systemLayoutSizeFittingSize method.

You can then use this to create the frame for your application. This technique works whenever you need to know the size of a view that uses autolayout internally.

The code in swift looks like

//Create the view
let tableHeaderView = CustomTableHeaderView()

//Set the content
tableHeaderView.textLabel.text = @"Hello world"

//Ask auto layout for the smallest size that fits my constraints    
let size = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

//Create a frame    
tableHeaderView.frame = CGRect(origin: CGPoint.zeroPoint, size: size)

//Set the view as the header    
self.tableView.tableHeaderView = self.tableHeaderView

Or in Objective-C

//Create the view
CustomTableHeaderView *header = [[CustomTableHeaderView alloc] initWithFrame:CGRectZero];

//Set the content
header.textLabel.text = @"Hello world";

//Ask auto layout for the smallest size that fits my constraints
CGSize size = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

//Create a frame
header.frame = CGRectMake(0,0,size.width,size.height);

//Set the view as the header  
self.tableView.tableHeaderView = header

It should also be noted that in this particular instance, overriding requiresConstraintBasedLayout in your subclass, does result in a layout pass being performed, however the results of this layout pass are ignored and the system frame set to the width of the tableView and 0 height.

Jonathan
  • 1,592
  • 1
  • 14
  • 28
5

Code:

  extension UITableView {

          func sizeHeaderToFit(preferredWidth: CGFloat) {
            guard let headerView = self.tableHeaderView else {
              return
            }

            headerView.translatesAutoresizingMaskIntoConstraints = false
            let layout = NSLayoutConstraint(
              item: headerView,
              attribute: .Width,
              relatedBy: .Equal,
              toItem: nil,
              attribute:
              .NotAnAttribute,
              multiplier: 1,
              constant: preferredWidth)

            headerView.addConstraint(layout)

            let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
            headerView.frame = CGRectMake(0, 0, preferredWidth, height)

            headerView.removeConstraint(layout)
            headerView.translatesAutoresizingMaskIntoConstraints = true

            self.tableHeaderView = headerView
          }
  }
Phil
  • 325
  • 3
  • 7
  • It works.Give proper autolayout constraints to all the tableheaderview subviews. If you miss a single constraint, it will not work. – abhimuralidharan Mar 26 '18 at 05:24
5

Extended this solution http://collindonnell.com/2015/09/29/dynamically-sized-table-view-header-or-footer-using-auto-layout/ for table footer view:

@interface AutolayoutTableView : UITableView

@end

@implementation AutolayoutTableView

- (void)layoutSubviews {
    [super layoutSubviews];

    // Dynamic sizing for the header view
    if (self.tableHeaderView) {
        CGFloat height = [self.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect headerFrame = self.tableHeaderView.frame;

        // If we don't have this check, viewDidLayoutSubviews() will get
        // repeatedly, causing the app to hang.
        if (height != headerFrame.size.height) {
            headerFrame.size.height = height;
            self.tableHeaderView.frame = headerFrame;
            self.tableHeaderView = self.tableHeaderView;
        }

        [self.tableHeaderView layoutIfNeeded];
    }

    // Dynamic sizing for the footer view
    if (self.tableFooterView) {
        CGFloat height = [self.tableFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect footerFrame = self.tableFooterView.frame;

        // If we don't have this check, viewDidLayoutSubviews() will get
        // repeatedly, causing the app to hang.
        if (height != footerFrame.size.height) {
            footerFrame.size.height = height;
            self.tableFooterView.frame = footerFrame;
            self.tableFooterView = self.tableFooterView;
        }

        self.tableFooterView.transform = CGAffineTransformMakeTranslation(0, self.contentSize.height - footerFrame.size.height);
        [self.tableFooterView layoutIfNeeded];
    }
}

@end
Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
k06a
  • 17,755
  • 10
  • 70
  • 110
5

Updated for Swift 4.2

extension UITableView {

    var autolayoutTableViewHeader: UIView? {
        set {
            self.tableHeaderView = newValue
            guard let header = newValue else { return }
            header.setNeedsLayout()
            header.layoutIfNeeded()
            header.frame.size = 
            header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
            self.tableHeaderView = header
        }
        get {
            return self.tableHeaderView
        }
    }
}
Community
  • 1
  • 1
Caleb Friden
  • 200
  • 2
  • 8
4

The following worked for me.

  1. Use a plain old UIView as the header view.
  2. Add subviews to that UIView
  3. Use autolayout on the subviews

The main benefit I see is limiting frame calculations. Apple should really update UITableView's API to make this easier.

Example using SnapKit:

let layoutView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 60))
layoutView.backgroundColor = tableView.backgroundColor
tableView.tableHeaderView = layoutView

let label = UILabel()
layoutView.addSubview(label)
label.text = "I'm the view you really care about"
label.snp_makeConstraints { make in
    make.edges.equalTo(EdgeInsets(top: 10, left: 15, bottom: -5, right: -15))
}
David Nix
  • 3,324
  • 3
  • 32
  • 51
4

Strange things happens. systemLayoutSizeFittingSize works great for iOS9, but doesn't for iOS 8 in my case. So this problems solves quite easy. Just get link to the bottom view in header and in viewDidLayoutSubviews after super call update header view bounds by inserting height as CGRectGetMaxY(yourview.frame) + padding

UPD: The easiest solution ever: So, in header view place subview and pin it to left, right, top. In that subview place your subviews with auto-height constraints. After that give all the job to the autolayout (no calculation required)

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    CGFloat height = CGRectGetMaxY(self.tableView.tableHeaderView.subviews.firstObject.frame);
    self.tableView.tableHeaderView.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height);
    self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}

As a result subview is expanding/shrinking like it should, at the end it calls viewDidLayoutSubviews. At the time we know the actual size of the view, so set headerView height and update it by re-assigning. Works like a charm!

Also works for footer view.

HotJard
  • 4,598
  • 2
  • 36
  • 36
4

you can add top + horizontal location constraint between header and tableview, to place it, correctly (if the header itself contains all the necessary internal layout constraints to have a correct frame)

in the tableViewController viewDidLoad method

    headerView.translatesAutoresizingMaskIntoConstraints = false

    tableView.tableHeaderView = headerView

    headerView.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
    headerView.topAnchor.constraint(equalTo: tableView.topAnchor).isActive = true
    headerView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor).isActive = true
GreatWiz
  • 735
  • 4
  • 8
  • This is all that is required! In fact you can leave out both topAnchor and centerXAnchor too – Dale Feb 16 '21 at 04:43
3

For most cases the best solution is simply not to fight the framework and embrace autoresizing masks:

// embrace autoresizing masks and let the framework add the constraints for you
headerView.translatesAutoresizingMaskIntoConstraints = true
headerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

// figure out what's the best size based on the table view width
let width = self.tableView.frame.width
let targetSize = headerView.systemLayoutSizeFitting(CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
headerView.frame.size = targetSize
self.tableView.tableHeaderView = headerView

By using autoresizing masks you're telling the framework how your view should change its size when the superview changes its size. But this change is based on the initial frame you've set.

pfandrade
  • 2,359
  • 15
  • 25
2

My solution is making a new class like this.

class BaseTableHeaderView: UIView {

    func sizeToFitBasedOnConstraints(width: CGFloat = Screen.width) {
        let size = systemLayoutSizeFitting(CGSize(width: width, height: 10000),
                                              withHorizontalFittingPriority: .required,
                                              verticalFittingPriority: .fittingSizeLevel)
        frame = CGRect(origin: .zero, size: size)
    }

    override func willMove(toSuperview newSuperview: UIView?) {
        sizeToFitBasedOnConstraints()
        super.willMove(toSuperview: newSuperview)
    }

}

To use it, simply add all your subviews onto an instance of BaseTableHeaderView and attach it to your table view.

let tableHeaderView = BaseTableHeaderView()
tableHeaderView.addSubview(...)
tableView.tableHeaderView = tableHeaderView

It will automatically resize based on its constraints.

1

My table header view is a UIView subclass - I created a single contentView UIView within the initializer, with its bounds the same as the table header view's frame and added all my objects as a subview of that.

Then add the constraints for your objects within the table header view's layoutSubviews method rather than within the initializer. That solved the crash.

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:CGRectMake(0, 0, 0, 44.0)];
    if (self) {
        UIView *contentView = [[UIView alloc] initWithFrame:self.bounds];
        contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth;

        // add other objects as subviews of content view

    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];

    // remake constraints here
}
Ryan
  • 5,416
  • 1
  • 39
  • 36
1

My AutoLayout is working very good:

CGSize headerSize = [headerView systemLayoutSizeFittingSize:CGSizeMake(CGRectGetWidth([UIScreen mainScreen].bounds), 0) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
headerView.frame = CGRectMake(0, 0, headerSize.width, headerSize.height);
self.tableView.tableHeaderView = headerView;
RomanV
  • 549
  • 4
  • 6
  • I didn't do this exactly but you gave me a good idea - removing the headerView, re-setting its frame, and adding it back. – dinesharjani May 06 '19 at 10:14
0

I know this is an old post but After going through all the SO posts regarding this and passing a whole afternoon playing with this, I finally came up with a clean and yet very simple solution

First of all, My view hierarchy looks like this:

  1. Table View
    1. View tableHeaderView
      1. View with an outlet called headerView

Now inside the View (No.3), I set up all the constraints as I would normally including the bottom space to container. This will make the container (i.e. 3.View i.e. headerView) to size itself based on it's subviews and their constraints.

After that, I set the constraints between 3. View and 2. View to these:

  1. Top Space to container: 0
  2. Leading Space to container: 0
  3. Trailing Space to container: 0

Notice that I omit intentionally the bottom space intentionally.

Once all of this is done in the storyboard, everything that's left to do is paste those three lines of codes:

if (self.headerView.frame.size.height != self.tableView.tableHeaderView.frame.size.height) {
    UIView *header = self.tableView.tableHeaderView;
    CGRect frame = self.tableView.tableHeaderView.frame;
    frame.size.height = self.headerView.frame.size.height + frame.origin.y;
    header.frame = frame;
    self.tableView.tableHeaderView = header;
}
0

Tips: If you use method setAndLayoutTableHeaderView, you should update subviews's frame,so in this situation UILabel's preferredMaxLayoutWidth should call before systemLayoutSizeFittingSize called, do not call in layoutSubview.

code show

0

Share my approach.

UITableView+XXXAdditions.m

- (void)xxx_setTableHeaderView:(UIView *)tableHeaderView layoutBlock:(void(^)(__kindof UIView *tableHeaderView, CGFloat *containerViewHeight))layoutBlock {
      CGFloat containerViewHeight = 0;
      UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectZero];
      [backgroundView addSubview:tableHeaderView];
      layoutBlock(tableHeaderView, &containerViewHeight);

      backgroundView.frame = CGRectMake(0, 0, 0, containerViewHeight);

      self.tableHeaderView = backgroundView;
}

Usage.

[self.tableView xxx_setTableHeaderView:myView layoutBlock:^(__kindof UIView * _Nonnull tableHeaderView, CGFloat *containerViewHeight) {
    *containerViewHeight = 170;

    [tableHeaderView mas_makeConstraints:^(MASConstraintMaker *make) {
      make.top.equalTo(@20);
      make.centerX.equalTo(@0);
      make.size.mas_equalTo(CGSizeMake(130, 130));
    }];
  }];
Vincent Sit
  • 2,214
  • 1
  • 24
  • 27
0

An old post. But a good post. Here's my 2 cents.

Firstly, ensure that your header view has its constraints arranged so that it can support it's own intrinsic content size. Then do the following.

//ViewDidLoad
headerView.translatesAutoresizingMaskIntoConstraints = false
headerView.configure(title: "Some Text A")

//Somewhere else
headerView.update(title: "Some Text B)

private var widthConstrained = false

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if widthConstrained == false {
        widthConstrained = true
        tableView.addConstraint(NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: tableView, attribute: .width, multiplier: 1, constant: 0))
        headerView.layoutIfNeeded()
        tableView.layoutIfNeeded()
    }
}

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: { (context) in
        self.headerView.layoutIfNeeded()
        self.tableView.layoutIfNeeded()
    }, completion: nil)
}
0

I was able to achieve it by the following approach (this works for footer the same way).

First, you will need small UITableView extension:

Swift 3

extension UITableView {
    fileprivate func adjustHeaderHeight() {
        if let header = self.tableHeaderView {
            adjustFrame(header)
        }
    }

    private func adjustFrame(_ view: UIView) {
        view.frame.size.height = calculatedViewHeight(view)
    }

    fileprivate func calculatedHeightForHeader() -> CGFloat {
        if let header = self.tableHeaderView {
            return calculatedViewHeight(header)
        }
        return 0.0
    }

    private func calculatedViewHeight(_ view: UIView) -> CGFloat {
        view.setNeedsLayout()
        let height = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        return height
    }
}

In your view controller class implementation:

// this is a UIView subclass with autolayout
private var headerView = MyHeaderView()

override func loadView() {
    super.loadView()
    // ...
    self.tableView.tableHeaderView = headerView
    self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension
    // ...
}

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    // this is to prevent recursive layout calls
    let requiredHeaderHeight = self.tableView.calculatedHeightForHeader()
    if self.headerView.frame.height != requiredHeaderHeight {
        self.tableView.adjustHeaderHeight()
    }
}

Notes about a header UIView's subview implementation:

  1. You have to 100% sure that your header view has the correct autolayout setup. I would recommend to start with simple header view with just one heigh constraint and try out the setup above.

  2. Override requiresConstraintBasedLayout and return true:

.

class MyHeaderView: UIView {
   // ...
   override static var requiresConstraintBasedLayout : Bool {
       return true
   }
   // ...
}
Yevhen Dubinin
  • 4,657
  • 3
  • 34
  • 57
0

For Xamarin users:

public override void ViewDidLayoutSubviews()
{
    base.ViewDidLayoutSubviews();

    TableviewHeader.SetNeedsLayout();
    TableviewHeader.LayoutIfNeeded();

    var height = TableviewHeader.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize).Height;
    var frame = TableviewHeader.Frame;
    frame.Height = height;
    TableviewHeader.Frame = frame;
}

Assuming you named the header view of your tableview as TableviewHeader

Gustavo Baiocchi Costa
  • 1,379
  • 3
  • 16
  • 34
0

Here is how you can do in your UIViewController

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if headerView.frame.size.height == 0 {
      headerView.label.preferredMaxLayoutWidth = view.bounds.size.width - 20
      let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height

      headerView.frame.size = CGSize(width: tableView.bounds.size.width, height: height)
    }
  }
onmyway133
  • 45,645
  • 31
  • 257
  • 263
0

Any constraint-based UIView can be a good tableHeaderView.

One needs to set a tableFooterView before and then impose additional trailing constraint on tableFooterView and tableHeaderView.

- (void)viewDidLoad {

    ........................
    // let self.headerView is some constraint-based UIView
    self.tableView.tableFooterView = [UIView new];
    [self.headerView layoutIfNeeded];
    self.tableView.tableHeaderView = self.headerView;

    [self.tableView.leadingAnchor constraintEqualToAnchor:self.headerView.leadingAnchor].active = YES;
    [self.tableView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
    [self.tableView.topAnchor constraintEqualToAnchor:self.headerView.topAnchor].active = YES;
    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;

}

One can find all details and code snippets here

malex
  • 9,874
  • 3
  • 56
  • 77
0

I have figured out a workaround. wrap your autolayout wrriten xib header view in an empty uiview wrapper, and assign the header view to tableView's tableViewHeader property.

    UIView *headerWrapper = [[UIView alloc] init];
    AXLHomeDriverHeaderView *headerView = [AXLHomeDriverHeaderView loadViewFromNib];
    [headerWrapper addSubview:headerView];
    [headerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(headerWrapper);
    }];
    self.tableView.tableHeaderView = headerView;
tounaobun
  • 14,570
  • 9
  • 53
  • 75
0

Here's what works for UITableViewController in ios 12,

Drap a UIView into the TableView above all the prototype cells for header and below all the prototype cells for footer. Setup your header and footer as needed. Set all the required constraints.

Now use the following extension methods

public static class UITableVIewExtensions
{

    public static void MakeHeaderAutoDimension(this UITableView tableView)
    {
        if (tableView.TableHeaderView is UIView headerView) {
            var size = headerView.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize);
            if (headerView.Frame.Size.Height != size.Height) {
                var frame = headerView.Frame;
                frame.Height = size.Height;
                headerView.Frame = frame;
                tableView.TableHeaderView = headerView;
                tableView.LayoutIfNeeded();
            }
        }
    }

    public static void MakeFooterAutoDimension(this UITableView tableView)
    {
        if (tableView.TableFooterView is UIView footerView) {
            var size = footerView.SystemLayoutSizeFittingSize(UIView.UILayoutFittingCompressedSize);
            if (footerView.Frame.Size.Height != size.Height) {
                var frame = footerView.Frame;
                frame.Height = size.Height;
                footerView.Frame = frame;
                tableView.TableFooterView = footerView;
                tableView.LayoutIfNeeded();
            }
        }
    }
}

and call it in ViewDidLayoutSubviews of the subclass of UITableViewController

public override void ViewDidLayoutSubviews()
{
    base.ViewDidLayoutSubviews();

    TableView.MakeHeaderAutoDimension();
    TableView.MakeFooterAutoDimension();
}
Drunken Daddy
  • 7,326
  • 14
  • 70
  • 104
0

I encountered the problem of getting width 375pt, the only way that worked for me is to relayout the tableView to get the correct width. I also preferred AutoLayout over setting Frame size.

Here's the version that works for me:

Xamarin.iOS

public static void AutoLayoutTableHeaderView(this UITableView tableView, UIView header)
{
    tableView.TableHeaderView = header;
    tableView.SetNeedsLayout();
    tableView.LayoutIfNeeded();
    header.WidthAnchor.ConstraintEqualTo(tableView.Bounds.Width).Active = true;       
    tableView.TableHeaderView = header;
}

Swift Version (modified from @Ben Packard answer)

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
        self.setNeedsLayout()
        self.layoutIfNeeded()
        header.widthAnchor.widthAnchor.constraint(equalTo: self.bounds.width).isActive = true
        self.tableHeaderView = header
    }
}
Community
  • 1
  • 1
Baron Ch'ng
  • 181
  • 8
0

I created a subclass of UITableView and used UIStackView for both header and footer, it also enables setting more than one view.

https://github.com/omaralbeik/StackableTableView

Omar Albeik
  • 1,332
  • 13
  • 17
0

After checking other solutions that used to work but no longer work I created a following solution for header view with multiline labels that works fine for me on iOS 12, iOS 13, iOS 14, iOS 15 in all cases also with device rotations. Instead of setting your view directly as a table header view use a wrapper view that automatically updates it heights if the real view changes the height. Add following class to your project:

class TableViewHeaderHelperView: UIView {
  private weak var headerView: UIView?
  private weak var tableView: UITableView?
  private var lastUpdatedHeight: CGFloat = 0

  init(headerView: UIView) {
    self.headerView = headerView
    super.init(frame: .zero)
    translatesAutoresizingMaskIntoConstraints = true
    addSubview(headerView)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  func addToTableView(_ tableView: UITableView) {
    self.tableView = tableView
    tableView.tableHeaderView = self
    headerView?.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
  }

  func removeFromTableView() {
    self.tableView?.tableHeaderView = nil
    self.tableView = nil
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    refreshHeaderHeightIfNeeded()
  }

  func refreshHeaderHeightIfNeeded() {
    DispatchQueue.main.async {
      let headerViewSize = self.headerView?.bounds.size ?? .zero
      if headerViewSize.height != self.lastUpdatedHeight {
        self.lastUpdatedHeight = headerViewSize.height
        self.frame.size = headerViewSize
        self.tableView?.tableHeaderView = self
      }
    }
  }
}

Then you can use this helper class as below:

let headerWrapperView = TableViewHeaderHelperView(headerView: yourRealHeaderView)
headerWrapperView.addToTableView(tableView)

Alternatively also I used a similar approach in my library LSCategories that provides various extensions including an extension for UITableView class which allows to set an autolayout view in a table header view or footer view with a single line of code so you can also try that instead.

Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
  • It fails in many case. Correct solution in all cases, for latest iOS phone and pad: https://stackoverflow.com/a/76540476/294884 – Fattie Jun 23 '23 at 13:15
  • @Fattie In which case it does not work for you? Do you have any sample project? I will investigate it in free time. – Leszek Szary Jun 23 '23 at 14:09
-1

Accepted answer is a only useful for tables with a single section. For multi-section UITableView just make sure that your header inherits from UITableViewHeaderFooterView and you will be fine.

As an alternative, just embed your current header in the contentView of a UITableViewHeaderFooterView. Exactly like UITableViewCell works.

redent84
  • 18,901
  • 4
  • 62
  • 85