Is it possible to use AutoLayout with UITableView's tableHeaderView?

80,321

Solution 1

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)

Solution 2

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;

Solution 3

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
    }

Solution 4

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];
}

Solution 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
        }
    }
}
Share:
80,321

Related videos on Youtube

ItsASecret
Author by

ItsASecret

@@!@#$@!@#!@#

Updated on January 13, 2022

Comments

  • ItsASecret
    ItsASecret over 2 years

    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.

    • jrturton
      jrturton almost 11 years
      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.
    • Mariam K.
      Mariam K. almost 11 years
      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.
    • ItsASecret
      ItsASecret almost 11 years
      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
    • Mariam K.
      Mariam K. almost 11 years
      Apple advises developers to use IB whenever possible when it comes to autolayout. It really helps in avoiding a lot of inconsistency problems.
    • malex
      malex almost 8 years
      The true complete autolayout solution is here
    • Elijah
      Elijah almost 6 years
      This answer solves this very eloquently. stackoverflow.com/questions/34661793/…
    • Schemetrical
      Schemetrical almost 5 years
      medium.com/@aunnnn/… This solves this issue almost perfectly.
  • ItsASecret
    ItsASecret almost 11 years
    I had that exception too but adding a category to UITableView fixed it I found it in that answer : stackoverflow.com/questions/12610783/…
  • ItsASecret
    ItsASecret almost 11 years
    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
    ItsASecret almost 11 years
    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
  • Rpranata
    Rpranata over 9 years
    question is about tableHeaderView not the section header.
  • ItsASecret
    ItsASecret over 9 years
    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!
  • Ben Packard
    Ben Packard over 9 years
    Yeah, just figured I should add it for posterity.
  • Nik
    Nik about 9 years
    Worked for me after I removed my header from storyboard (from table view) and placed it into separate xib
  • Benjohn
    Benjohn almost 9 years
    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
    Benjohn almost 9 years
    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:.
  • Laszlo
    Laszlo over 8 years
    NOTE: if you experiencing that the header is above the first cells, then you forgot to reset the header property to self.tableView.tableHeaderView
  • Martin
    Martin over 8 years
    @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 stackoverflow.com/a/21099430/127493 by calling its - (void)sizeHeaderToFit in viewDidLayoutSubviews
  • TylerJames
    TylerJames over 8 years
    Often it amazes me how tricky it can be to do something completely trivial like this.
  • HotJard
    HotJard over 7 years
    Jesus! label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame) is very important! Thanks
  • docchang
    docchang over 7 years
    Spent a day yesterday trying to get the tableHeader to auto resize/layout correctly. This solution works for me. Thanks a bunch.
  • JakubKnejzlik
    JakubKnejzlik over 7 years
    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
  • Daumantas Versockas
    Daumantas Versockas over 7 years
    @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);)
  • Kesong Xie
    Kesong Xie about 7 years
    Just Use header.setNeedsLayout() header.layoutIfNeeded() header.frame.size = header.systemLayoutSizeFitting(UILayoutFittingCompressedSize‌​) self.tableHeaderView = header would work at iOS 10.2
  • Simon
    Simon about 7 years
    This loops for me in iOS 10.
  • smirkingman
    smirkingman over 6 years
    This also worked for us on iOS 11 with a dynamic height header with multi-line labels.
  • d4Rk
    d4Rk over 6 years
    You can also remove the flexibleHeight-Autoresizing-Option in IB of course.
  • abhimuralidharan
    abhimuralidharan about 6 years
    It works.Give proper autolayout constraints to all the tableheaderview subviews. If you miss a single constraint, it will not work.
  • marvin_yorke
    marvin_yorke almost 6 years
    Hi! Could you please explain self.tableFooterView.transform part? Why is it necessary?
  • k06a
    k06a almost 6 years
    @mrvn transform is used to move footer to the bottom of the tableView.
  • dinesharjani
    dinesharjani almost 5 years
    I didn't do this exactly but you gave me a good idea - removing the headerView, re-setting its frame, and adding it back.
  • tounaobun
    tounaobun almost 5 years
    Plus for adding multi-line label handle cases.
  • vikzilla
    vikzilla almost 5 years
    I've been trying to set the height of my tableFooterView (via a xib / nib) and wasn't having success setting the frame, height, layoutIfNeeded(), etc. But this solution finally allowed me to set it.
  • Denis Kutlubaev
    Denis Kutlubaev about 4 years
    Don't forget to set height constraint to whole view in a xib file.
  • user
    user over 3 years
    Nice. Autoresize masks are heavily underrated.
  • Dale
    Dale about 3 years
    This is all that is required! In fact you can leave out both topAnchor and centerXAnchor too