6

I've been searching for a clear answer on how to add auto layout to a UITableView. So far, my code looks like:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    UINib *nib = [UINib nibWithNibName:@"HomeHeaderView" bundle:nil];
    UIView *headerView = (UIView *)[nib instantiateWithOwner:self options:nil][0];
    [headerView.layer setCornerRadius:6.0];
    [headerView setTranslatesAutoresizingMaskIntoConstraints:NO];

//    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(headerView);
//    NSMutableArray *headerConstraints = [[NSMutableArray alloc] init];
//    [headerConstraints addObject:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[headerView]-|" options:0 metrics:nil views:viewsDictionary]];
//        [headerConstraints addObject:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[headerView]-|" options:0 metrics:nil views:viewsDictionary]];
//    [self.actionsTableView addConstraints:headerConstraints];
//    [self.view addSubview:headerView];
    tableView.tableHeaderView = headerView;
    [headerView layoutSubviews];

    NSLayoutConstraint *centerX = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0];
    NSLayoutConstraint *centerY = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0];
    NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1 constant:300];
    NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:headerView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1 constant:90];
    [self.view addConstraints:@[centerX, centerY, width, height]];

    return headerView;
}

I basically have a nib file for my header view and I want to center that nib in my UITableViewHeader. I'd like it to grow and shrink accordingly in portrait and landscape orientations. I'm honestly unsure if I set up the constraint properly. I was not sure if my toItem was supposed to be the view controller's view, or the tableview itself.

I also did not know if I was supposed to add the headerview as a subview to either the view controller's view, or the tableview itself.

Or, I wasn't sure if setting tableView.tableHeaderView = headerView was enough.

I really have no clue what the best practices are for something like this. I wasn't sure if it all could be done in IB as well. Currently, with the code you see, I get this error:

'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super.'

It's because of that error, that I added [headerView layoutSubviews]

Thoughts on this? Thanks in advance!

Crystal
  • 28,460
  • 62
  • 219
  • 393

3 Answers3

3

The real problem is that you've confused viewForHeaderInSection: with the table's headerView. They are unrelated.

The former is the header for a section. You return the view from the delegate method.

That latter is the header for the table. You set the view, probably in your viewDidLoad.

Constraints operate in the normal way. But they should only be internal constraints to their subviews. At the time you form it, the view is not in your interface. And its size and place are not up to you at that time. If it's the section header, it will be resized automatically to fit correctly (in accordance with the table's width and the table's or delegate's statement of the header height). If it's the table header, you can give it an absolute height, but its width will be resized to fit correctly.

Here is a complete example of constructing a section header with internal constraints on its subviews.

- (UIView *)tableView:(UITableView *)tableView 
        viewForHeaderInSection:(NSInteger)section {
    UITableViewHeaderFooterView* h =
        [tableView dequeueReusableHeaderFooterViewWithIdentifier:@"Header"];
    if (![h.tintColor isEqual: [UIColor redColor]]) {
        h.tintColor = [UIColor redColor];
        h.backgroundView = [UIView new];
        h.backgroundView.backgroundColor = [UIColor blackColor];
        UILabel* lab = [UILabel new];
        lab.tag = 1;
        lab.font = [UIFont fontWithName:@"Georgia-Bold" size:22];
        lab.textColor = [UIColor greenColor];
        lab.backgroundColor = [UIColor clearColor];
        [h.contentView addSubview:lab];
        UIImageView* v = [UIImageView new];
        v.tag = 2;
        v.backgroundColor = [UIColor blackColor];
        v.image = [UIImage imageNamed:@"us_flag_small.gif"];
        [h.contentView addSubview:v];
        lab.translatesAutoresizingMaskIntoConstraints = NO;
        v.translatesAutoresizingMaskIntoConstraints = NO;
        [h.contentView addConstraints:
         [NSLayoutConstraint 
          constraintsWithVisualFormat:@"H:|-5-[lab(25)]-10-[v(40)]"
          options:0 metrics:nil views:@{@"v":v, @"lab":lab}]];
        [h.contentView addConstraints:
         [NSLayoutConstraint 
          constraintsWithVisualFormat:@"V:|[v]|"
           options:0 metrics:nil views:@{@"v":v}]];
        [h.contentView addConstraints:
         [NSLayoutConstraint
          constraintsWithVisualFormat:@"V:|[lab]|"
           options:0 metrics:nil views:@{@"lab":lab}]];
    }
    UILabel* lab = (UILabel*)[h.contentView viewWithTag:1];
    lab.text = self.sectionNames[section];
    return h;
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • can you provide code snippets on how to proceed? i'm still confused on how i would do constraints on either the section header or the tableview header at this point :-\. – Crystal Sep 18 '13 at 03:38
  • You do them in the normal way. But they should only be internal constraints to their subviews. At the time you form it, the view is not in your interface. If it's the section header, it will be resized automatically to fit correctly. If it's the table header, you can give it an absolute height, but its width will be resized to fit correctly. – matt Sep 18 '13 at 12:44
  • What about `heightForHeaderInSection`? Normally I'd say that I don't need to povide a height when using auto layout. But I've to provide a height. Otherwise the section header is not shown ... – testing Oct 29 '14 at 14:45
  • 2
    Unfortunately, it won't work. `UITableViewHeaderFooterView`'s `contentView` has computed size, and you will get `Unable to simultaneously satisfy constraints` warnings at runtime. It looks like while cell height can be automatically computed using AutoLayout, it can't be done for section headers. Just checked it on iOS 8.1. – Darrarski Dec 03 '14 at 20:05
  • @Darrarski It does work; it just doesn't calculate the height automatically. But you can still use the constraints yourself to calculate the height. I do it all the time. – matt Dec 03 '14 at 21:36
  • 2
    @matt if you set the constraints in a way that should stretch header to fit its content - it won't work on iOS 8. You will get `Unable to simultaneously satisfy constraints` like I said. If you add your custom subviews directly to header view instead of it's `contentView` - it will work. – Darrarski Dec 03 '14 at 21:41
  • @Darrarski Well, I think you're having some other problem. But we can't solve that in these comments. If you don't like my answer, downvote it. I don't care. – matt Dec 03 '14 at 21:46
  • 4
    @matt it's not about liking something or not :-) You explained how to add custom views with constraints to header view pretty well. I agree that this is the proper way to do so. Unfortunately, it will break constraints managed automatically by iOS 8. I just showed how to solve that problem to achieve auto-sizing headers. Instead of defining fixed height in `heightForHeaderInSection` you can return `UITableViewAutomaticDimension` and it will adjust header height automatically. – Darrarski Dec 03 '14 at 21:51
3

I found that solution provided by matt might not be the perfect, because he's adding custom views and constraints to UITableViewHeaderFooterView's contentView. That is always causing Auto Layout warnings in runtime: Unable to simultaneously satisfy constraints when we want to have dynamic header height.

I am not sure about the reason, but we can assume that iOS adds some extra constrains to contentView that sets fixed width and height of that view. Warnings generated in runtime tells that constraints we added manually can't be satisfied with those, and it's obvious because our constraints should stretch header view so the subviews can fit in it.

Solution is pretty easy - don't use UITableViewHeaderFooterView's contentView, just add your subviews directly to UITableViewHeaderFooterView. I can confirm that it's working without any issues on iOS 8.1. If you want to add several views and change the background color of you header, consider adding UIView that fills header view (thanks to AutoLayout constraints) and then all the subviews you would like to have to that view (I am calling it customContentView). That way we can avoid any AutoLayout issues and have auto-sizing headers in UITableView.

Community
  • 1
  • 1
Darrarski
  • 3,882
  • 6
  • 37
  • 59
3

This is a neat solution:

Optional: initWithStyle:UITableViewStyleGrouped to prevent floating tableViewHeader

Make two properties, the label is just for demonstration:

@property (nonatomic, strong, readwrite) UIView *headerView;
@property (nonatomic, strong, readwrite) UILabel *headerLabel;

Setup everything in viewDidLoad:

self.headerView = [[UIView alloc] initWithFrame:CGRectZero];
self.headerLabel = [[UILabel alloc] init];
self.headerLabel.text = @"Test";
self.headerLabel.numberOfLines = 0; //unlimited
self.headerLabel.textAlignment = NSTextAlignmentCenter;
self.headerLabel.translatesAutoresizingMaskIntoConstraints = NO; //always set this to NO when using AutoLayout
[self.headerView addSubview:self.headerLabel];

NSString *horizontalFormat = @"H:|-[headerLabel]-|";
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:horizontalFormat options:0 metrics:nil views:@{@"headerLabel":self.headerLabel}];
[self.headerView addConstraints:horizontalConstraints];

NSString *verticalFormat = @"V:|-[headerLabel]-|";
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:verticalFormat options:0 metrics:nil views:@{@"headerLabel":self.headerLabel}];
[self.headerView addConstraints:verticalConstraints];

In viewForHeaderInSection:

return self.headerView;

In heightForHeaderInSection:

self.headerLabel.preferredMaxLayoutWidth = tableView.bounds.size.width;
return [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
Kampai
  • 22,848
  • 21
  • 95
  • 95
Nico S.
  • 3,056
  • 1
  • 30
  • 64