36

I am trying to create a header view for my uiTableView (not a section header, I already have those.) I have set up an XIB in interface builder. All the connections are hooked up and it runs beautifully... except the table doesn't give it enough room! My problem is that the top of the table overlaps the table's header by a little.

My XIB is setup with autlayout for all the buttons, and IB is happy that the constraints don't conflict / ambiguous. The view is set to Freeform size, which in my case ended up being 320 x 471. Then in constraints for the view, I set an intrinsic size for the view of the same.

Now this works perfectly with my table. Everything looks great. But if I manually change any of the fonts in the header view with code, the layout makes the view bigger, and it ends up underneath my table.

Any ideas how to get the tableviewcontroller to leave enough room for the header view after setting fonts and sizes? I hope I've made sense explaining this.

jedipixel
  • 491
  • 1
  • 5
  • 5
  • any chance of a screen grab? – Max MacLeod Sep 25 '13 at 12:54
  • Maybe this solves the problem: http://stackoverflow.com/a/16324668/570339 – Ramy Al Zuhouri Sep 25 '13 at 12:56
  • The header view is only sized wrong if I change the font and size of various UI elements in the viewcontroller code. [screenshots](http://http://imgur.com/a/NI9D3#0) available. The header of the table ends with the large photo. then the table begins – jedipixel Sep 25 '13 at 14:08
  • I am fiddling with my autolayout constraints in case maybe they're the problem. They all compile without warnings, but maybe theres a setting I've missed. – jedipixel Sep 25 '13 at 15:27
  • I am wrestling with this issue as well. My header view is supposed to expand to the height of its contents, but it does not. Did you ever find a solution? – Greg W Nov 18 '13 at 15:31
  • I find that auto layout doesn't work well with table header or footer views when it comes to height resizing. All of the other inter-view spacing of subviews is fine, but the only way I get a table header or footer view to change height, is an explicit frame setting. Unfortunate, but I've found no workable alternative yet. – idStar Mar 07 '14 at 19:43
  • The true complete autolayout solution is [here](http://stackoverflow.com/a/38386985/2066428) – malex Jul 15 '16 at 02:33

11 Answers11

67

Note: A Swift 3+ version can be found here: https://gist.github.com/marcoarment/1105553afba6b4900c10#gistcomment-1933639


The idea is to calculate header's height with help of systemLayoutSizeFittingSize:targetSize.

Returns the size of the view that satisfies the constraints it holds. Determines the best size of the view considering all constraints it holds and those of its subviews.

After changing header's height it is necessary to reassign tableHeaderView property to adjust table cells.

Based on this answer: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights

- (void)sizeHeaderToFit
{
    UIView *header = self.tableView.tableHeaderView;

    [header setNeedsLayout];
    [header layoutIfNeeded];

    CGFloat height = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    CGRect frame = header.frame;

    frame.size.height = height;
    header.frame = frame;

    self.tableView.tableHeaderView = header;
}
iwasrobbed
  • 46,496
  • 21
  • 150
  • 195
vokilam
  • 10,153
  • 3
  • 45
  • 56
  • While it would be nice for AutoLayout to just extend the height of the table header or footer based on constraints, using the above approach you've provided @vokilam, is the cleanest I've seen (and it worked for me). – idStar Mar 07 '14 at 19:47
  • 2
    What helped me was reading and reassigning: `UIView *header = self.tableView.tableHeaderView;` and `self.tableView.tableHeaderView = header;`. I'm curious why. – gabriel14 Aug 06 '14 at 13:43
  • 2
    Is there a way to trigger execution of this code by observing autolayout rather then calling this function directly from all places that may change autolayout? – kjam Jan 15 '15 at 14:11
  • 4
    Works absolutely perfectly for me - the only thing I didn't see in the answer is where to call this. I put a call in `viewDidLayoutSubviews` and it worked for me. – Josh Brown Apr 16 '15 at 15:48
  • Actually, it turns out that `viewDidAppear` is a safer place to do this, especially if you're subclassing `UITableViewController`. I wrote a full, more in-depth [tutorial on table header view sizing in Swift](http://roadfiresoftware.com/2015/05/how-to-size-a-table-header-view-using-auto-layout-in-interface-builder/) that should help. – Josh Brown May 05 '15 at 18:33
  • I like your approach, but it does not work for me... The height found is correct. But even but manually setting the header height Mr. Autolayout don't even care and enlarge my view as he wants. I hate you Mr. Autolayout. – Martin May 21 '15 at 15:26
  • 4
    This does work for me as well...on an iPhone 6 Plus with iOS 9.1, but doesn't work on my iPhone 5s iOS 8.0, or iPhone 4s iOS 8.1. Placing this call in viewDidLayoutSubviews does really weird things, it keeps calling viewDidLayoutSubview recursively...and placing the code somewhere else doesn't work either, I've placed the code somewhere I know is called after viewDidLayoutSubviews. Any ideas? – ClockWise Nov 12 '15 at 15:05
  • Neat solution! Here I got a 1px-line glitch between tableHeaderView and the first section (Not even a logic 1px, an actual 1px). It turns out the height calculation was 277.5, and adding `frame.size.height = round(height)` did the trick. – Bruno Galinari Apr 04 '16 at 18:39
  • 2
    @ClockWise I found such behavior when you're using `UITableViewController`, because it calls layout again every time when you set tableView header. I recommend to switch to `UIViewController` and embedded `UITableView` inside it, that will eliminate the problem. – Vasily Aug 24 '16 at 07:07
15

I encountered a similar problem when I was trying to set my table view header to a custom view I defined with auto layout using interface builder.

When I did so, I would find that it would be obscured by a section header.

My work around involved using a "dummy view" as the table header view and then adding my custom view to that dummy view as a subview. I think this allows auto layout to configure the appearance as per the constraints and the containing view's frame. This is probably not as elegant as vokilam's solution, but it works for me.


CGRect headerFrame = CGRectMake(0, 0, yourWidth, yourHeight);
UIView *tempView = [[UIView alloc] initWithFrame:headerFrame];
[tempView setBackgroundColor:[UIColor clearColor]];
YourCustomView *customView = [[YourCustomView alloc] initWithFrame: headerFrame];
[tempView addSubview:movieHeader];
self.tableView.tableHeaderView = tempView;
rmigneco
  • 595
  • 4
  • 16
  • 1
    although not the cleanest, this worked for me, was quick, and allowed me to move on with my life – echappy May 14 '17 at 19:08
9

Because your tableHeaderView is inited form xib with auto layout, so the constraints of your custom headerView and the superView is unknown.You should add the constraints of your custom headerView:

1.in viewDidLLoad assign your custom headerView to the tableView's tableHeaderView

 - (void)viewDidLoad
{
    [super viewDidLoad];
    UIView *yourHeaderView = [[[NSBundle mainBundle] loadNibNamed:@"yourHeaderView" owner:nil options:nil] objectAtIndex:0];
    //important:turn off the translatesAutoresizingMaskIntoConstraints;apple documents for details
     yourHeaderView.translatesAutoresizingMaskIntoConstraints = NO;
    self.tableView.tableHeaderView = yourHeaderView;
}

2.in - (void)updateViewConstraints,add the essential constraints of your custom headerView

- (void)updateViewConstraints
{
    NSDictionary *viewsDictionary = @{@"headerView":yourHeaderView};

    NSArray *constraint_V = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[headerView(121)]"
                                                                    options:0
                                                                    metrics:nil
                                                                      views:viewsDictionary];

    NSArray *constraint_H = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[headerView(320)]"
                                                                    options:0
                                                                    metrics:nil
                                                                      views:viewsDictionary];
    [self.headerView addConstraints:constraint_H];
    [self.headerView addConstraints:constraint_V];

    NSArray *constraint_POS_V = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[headerView]"
                                                                    options:0
                                                                    metrics:nil
                                                                      views:viewsDictionary];

    NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[headerView]"
                                                                    options:0
                                                                    metrics:nil
                                                                      views:viewsDictionary];
    [self.tableView addConstraints:constraint_POS_V];
    [self.tableView addConstraints:constraint_POS_H];

    [super updateViewConstraints];
}

OK! PS:Here is the related document:Resizing the View Controller’s Views

Pavel Gurov
  • 5,587
  • 3
  • 26
  • 23
mayqiyue
  • 1,155
  • 1
  • 10
  • 16
8

What solved my issue was this:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    sizeHeaderToFit()
}

private func sizeHeaderToFit() { 
    if let headerView = tableView.tableHeaderView {

        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        var newFrame = headerView.frame

        // Needed or we will stay in viewDidLayoutSubviews() forever
        if height != newFrame.size.height {
            newFrame.size.height = height
            headerView.frame = newFrame

            tableView.tableHeaderView = headerView
        }
    }
}

I found this solution somewhere and worked like charm, I can't remember where though.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
ClockWise
  • 1,509
  • 15
  • 32
4

I found vokilam's answer to work great. Here's his solution in Swift.

func sizeHeaderToFit() {
    guard let header = tableView.tableHeaderView else { return }
    header.setNeedsLayout()
    header.layoutIfNeeded()
    let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
    header.frame.size.height = height
    tableView.tableHeaderView = header
}
Mr Rogers
  • 6,091
  • 2
  • 32
  • 34
  • 1
    Just as a heads up, in Swift you no longer need to create a placeholder variable when changing frame values. `header.frame.height = height` will work fine. – Chris C Mar 02 '16 at 10:01
  • Oh cool, thanks! I updated it with just one tweak `header.frame.size.height = height`. Thanks for the heads up. – Mr Rogers Mar 02 '16 at 18:55
2

The other answers pointed me in the right direction for getting the tableHeaderView to resize appropriately. However, I was getting a gap between the header and the tableView cells. This could also cause your cells to overlap with your header, depending on if the header grew or got smaller.

I went from this:

 tableView.reloadData()
 let height = tableViewHeader.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
 tableView.tableHeaderView?.frame.size.height = height

To fix the gap/overlap all I had to do was move tableView.reloadData() to after I set the new height instead of before.

To this:

 let height = tableViewHeader.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
 tableView.tableHeaderView?.frame.size.height = height
 tableView.reloadData()
templeman15
  • 799
  • 8
  • 14
1

In my case systemLayoutSizeFittingSize return incorrect size, because one of the subviews use aspect ratio (width to height) 4:1 constraint.

After I change it to constant height constraint value (100), it begin to works as expected.

Vitalii Gozhenko
  • 9,220
  • 2
  • 48
  • 66
1

Combining answers by @SwiftArchitect, @vokilam, and @mayqiyue, and using a programmatically created tableHeaderView instead of a nib (although, I don't see any reason why it shouldn't work with a nib), and under iOS 10.x/Xcode 8.2.1, here is what I have working:

Create the view in your view controller's -viewDidLoad or -init methods, or wherever you're using the table view:

MyCustomTableHeaderView *myCustomTableHeaderView = [[MyCustomTableHeaderView alloc] init]; // Don't set its frame
myCustomTableHeaderView.translatesAutoresizingMaskIntoConstraints = NO;
self.myTableView.tableHeaderView = myCustomTableHeaderView;
self.myCustomTableHeaderView = myCustomTableHeaderView;  // We set and update our constraints in separate methods so we store the table header view in a UIView property to access it later

Wherever you set up your custom header view's constraints (could be in `updateConstraints but this method could be called multiple times):

// We use Pure Layout in our project, but the workflow is the same:
// Set width and height constraints on your custom view then pin it to the top and left of the table view
// Could also use VFL or NSLayoutConstraint methods
[self.myCustomTableHeaderView autoSetDimension:ALDimensionWidth toSize:CGRectGetWidth(self.superview.frame)]; // Width set to tableview's width
[self.myCustomTableHeaderView autoSetDimension:ALDimensionHeight toSize:height]; // Whatever you want your height to be
[self.myCustomTableHeaderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
[self.myCustomTableHeaderView autoPinEdgeToSuperviewEdge:ALEdgeLeft];

// This is important
[self.myCustomTableHeaderView layoutIfNeeded]; // We didn't need to call -setNeedsLayout or reassign our custom header view to the table view's tableHeaderView property again, as was noted in other answers
Evan R
  • 875
  • 13
  • 27
0

sizeHeaderToFit suggested by @vokilam didn't work for me.

What worked is a modified version of @mayqiyue, invoked during viewDidLoad, wired height, equal width to table view.

// Anchor the headerview, and set width equal to superview
NSDictionary *viewsDictionary = @{@"headerView":self.tableView.tableHeaderView,
                                  @"tableView":self.tableView};

[self.tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[headerView]"
                                                                       options:0
                                                                       metrics:nil
                                                                         views:viewsDictionary]];
[self.tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[headerView]"
                                                                       options:0
                                                                       metrics:nil
                                                                         views:viewsDictionary]];
[self.tableView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[headerView(==tableView)]"
                                                                       options:0
                                                                       metrics:nil
                                                                         views:viewsDictionary]];
[self.tableView.tableHeaderView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[headerView(121)]"
                                                                                       options:0
                                                                                       metrics:nil
                                                                                         views:viewsDictionary]];
SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
0

Below code has worked for me :- Add this in your UIView class -

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];

if (self) {
    self.frame = CGRectMake(0, 0, self.frame.size.width, [[self class] height]);
}

return self;

}

0

@rmigneco answer worked for me, so here's the swift version of it:

let headerFrame = CGRectMake(0, 0, 100, 1);
let tempView = UIView(frame: headerFrame);
tempView.backgroundColor = UIColor.clearColor()

let yourCustomView = CustomView(frame: headerFrame)
tempView.addSubview(yourCustomView)

self.tableView.tableHeaderView = tempView
gmogames
  • 2,993
  • 1
  • 28
  • 40