Comment définir la hauteur de tableHeaderView (UITableView) avec mise en page automatique?

86

Je me fracasse la tête contre le mur avec ça depuis 3 ou 4 heures et je n'arrive pas à comprendre. J'ai un UIViewController avec un UITableView plein écran à l'intérieur (il y a d'autres choses à l'écran, c'est pourquoi je ne peux pas utiliser un UITableViewController) et je veux que mon tableHeaderView soit redimensionné avec la mise en page automatique. Inutile de dire que cela ne coopère pas.

Voir la capture d'écran ci-dessous.

entrez la description de l'image ici

Parce que le overviewLabel (par exemple le texte "Liste des informations de vue d'ensemble ici.") A un contenu dynamique, j'utilise la mise en page automatique pour le redimensionner et c'est superview. J'ai tout redimensionné bien, à l'exception du tableauHeaderView, qui se trouve juste en dessous de Paralax Table View dans le hiearchy.

Le seul moyen que j'ai trouvé pour redimensionner cette vue d'en-tête est par programme, avec le code suivant:

CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = headerFrameHeight;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];

In this case, headerFrameHeight is a manual calculation of the tableViewHeader height as follows (innerHeaderView is the white area, or the second "View", headerView is tableHeaderView):

CGFloat startingY = self.innerHeaderView.frame.origin.y + self.overviewLabel.frame.origin.y;
CGRect overviewSize = [self.overviewLabel.text
                       boundingRectWithSize:CGSizeMake(290.f, CGFLOAT_MAX)
                       options:NSStringDrawingUsesLineFragmentOrigin
                       attributes:@{NSFontAttributeName: self.overviewLabel.font}
                       context:nil];
CGFloat overviewHeight = overviewSize.size.height;
CGFloat overviewPadding = ([self.overviewLabel.text length] > 0) ? 10 : 0; // If there's no overviewText, eliminate the padding in the overall height.
CGFloat headerFrameHeight = ceilf(startingY + overviewHeight + overviewPadding + 21.f + 10.f);

The manual calculation works, but it's clunky and prone to error if things change in the future. What I want to be able to do is have the tableHeaderView auto-resize based on the provided constraints, like you can anywhere else. But for the life of me, I can't figure it out.

There's several posts on SO about this, but none are clear and ended up confusing me more. Here's a few:

It doesn't really make sense to change the translatesAutoresizingMaskIntoConstraints property to NO, since that just causes errors for me and doesn't make sense conceptually anyway.

Any help would really be appreciated!

EDIT 1: Thanks to TomSwift's suggestion, I was able to figure it out. Instead of manually calculating the height of the overview, I can have it calculated for me as follows and then re-set the tableHeaderView as before.

[self.headerView setNeedsLayout];
[self.headerView layoutIfNeeded];
CGFloat height = [self.innerHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + self.innerHeaderView.frame.origin.y; // adding the origin because innerHeaderView starts partway down headerView.

CGRect headerFrame = self.headerView.frame;
headerFrame.size.height = height;
self.headerView.frame = headerFrame;
[self.listTableView setTableHeaderView:self.headerView];

Edit 2: As others have noted, the solution posted in Edit 1 doesn't seem to work in viewDidLoad. It does, however, seem to work in viewWillLayoutSubviews. Example code below:

// Note 1: The variable names below don't match the variables above - this is intended to be a simplified "final" answer.
// Note 2: _headerView was previously assigned to tableViewHeader (in loadView in my case since I now do everything programatically).
// Note 3: autoLayout code can be setup programatically in updateViewConstraints.
- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];

    [_headerWrapper setNeedsLayout];
    [_headerWrapper layoutIfNeeded];
    CGFloat height = [_headerWrapper systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    CGRect headerFrame = _headerWrapper.frame;
    headerFrame.size.height = height;
    _headerWrapper.frame = headerFrame;
    _tableView.tableHeaderView = _headerWrapper;
}
Andrew Cross
la source
2
setTableHeaderView does not work on Xcode6. The problem is that cells are overlapped by the tableHeaderView. However, it works on Xcode5
DawnSong
@DawnSong do know a solution for Xcode6 so that I can update the answer?
Andrew Cross
1
After a lot of trial, I use UITableViewCell instead of tableHeaderView, and it works.
DawnSong
I smashed my head too!
Akshit Zaveri
The true complete autolayout solution is here
malex

Réponses:

35

You need to use the UIView systemLayoutSizeFittingSize: method to obtain the minimum bounding size of your header view.

I provide further discussion on using this API in this Q/A:

How to resize superview to fit all subviews with autolayout?

TomSwift
la source
It's coming back with height = 0, although I'm not 100% sure I've configured it right since this isn't a UITableViewCell, so there isn't the contentView to call "systemLayoutSizeFittingSize" on. What I tried: [self.headerView setNeedsLayout]; [self.headerView layoutIfNeeded]; CGFloat height = [self.headerView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; CGRect headerFrame = self.headerView.frame; headerFrame.size.height = height; self.headerView.frame = headerFrame; [self.listTableView setTableHeaderView:self.headerView]; I also confirmed that preferredMax... is valid.
Andrew Cross
Aha! Figured it out, I needed to do [self.innerHeaderView systemLayoutSizeFittingSize...] instead of self.headerView as that's what has the actual dynamic content. Thanks a ton!
Andrew Cross
It works but in my app there is a small height discrepancy. Probably because my labels are using an attributed text and Dynamic Type.
bio
23

I really battled with this one and plonking the setup into viewDidLoad didn't work for me since the frame is not set in viewDidLoad, I also ended up with tons of messy warnings where the encapsulated auto layout height of the header was being reduced to 0. I only noticed the issue on iPad when presenting a tableView in a Form presentation.

What solved the issue for me was setting the tableViewHeader in viewWillLayoutSubviews rather than in viewDidLoad.

func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if tableView.tableViewHeaderView == nil {
            let header: MyHeaderView = MyHeaderView.createHeaderView()
            header.setNeedsUpdateConstraints()
            header.updateConstraintsIfNeeded()
            header.frame = CGRectMake(0, 0, CGRectGetWidth(tableView.bounds), CGFloat.max)
            var newFrame = header.frame
            header.setNeedsLayout()
            header.layoutIfNeeded()
            let newSize = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
            newFrame.size.height = newSize.height
            header.frame = newFrame
            self.tableView.tableHeaderView = header
        }
    }
Daniel Galasko
la source
for updated solution with minimal code try this : stackoverflow.com/a/63594053/3933302
Sourabh Sharma
23

I've found an elegant way to way to use auto layout to resize table headers, with and without animation.

Simply add this to your View Controller.

func sizeHeaderToFit(tableView: UITableView) {
    if let headerView = tableView.tableHeaderView {
        let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
        var frame = headerView.frame
        frame.size.height = height
        headerView.frame = frame
        tableView.tableHeaderView = headerView
        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()
    }
}

To resize according to a dynamically changing label:

@IBAction func addMoreText(sender: AnyObject) {
    self.label.text = self.label.text! + "\nThis header can dynamically resize according to its contents."
}

override func viewDidLayoutSubviews() {
    // viewDidLayoutSubviews is called when labels change.
    super.viewDidLayoutSubviews()
    sizeHeaderToFit(tableView)
}

To animate a resize according to a changes in a constraint:

@IBOutlet weak var makeThisTallerHeight: NSLayoutConstraint!

@IBAction func makeThisTaller(sender: AnyObject) {

    UIView.animateWithDuration(0.3) {
        self.tableView.beginUpdates()
        self.makeThisTallerHeight.constant += 20
        self.sizeHeaderToFit(self.tableView)
        self.tableView.endUpdates()
    }
}

See the AutoResizingHeader project to see this in action. https://github.com/p-sun/Swift2-iOS9-UI

AutoResizingHeader

p-sun
la source
Hey p-sun. Thanks for great example. Seems I have issue with tableViewHeader. I tried to have the same constraints like in your example, but not working. How exactly I should add constraints to label which i want to increase height ? Thanks for any suggestion
Bonnke
1
Make sure you hook the label you want to make taller to @IBOutlet makeThisTaller and @IBAction fun makeThisTaller like in the example. Also, constrain all sides of your label to the tableViewHeader (e.g. top, bottom, left, and right).
p-sun
Thanks a lot! Finally solved by adding this line: lblFeedDescription.preferredMaxLayoutWidth = lblFeedDescription.bounds.width where label is that one which I want to increase size. Thanks !
Bonnke
Thanks! If you're doing this inside a View instead of a ViewController, then instead of overriding viewDidLayoutSubviews, you can override layoutSubviews
Andy Weinstein
4

This solution resizes the tableHeaderView and avoids infinite loop in the viewDidLayoutSubviews() method I was having with some of the other answers here:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let headerView = tableView.tableHeaderView {
        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        var headerFrame = headerView.frame

        // comparison necessary to avoid infinite loop
        if height != headerFrame.size.height {
            headerFrame.size.height = height
            headerView.frame = headerFrame
            tableView.tableHeaderView = headerView
        }
    }
}

See also this post: https://stackoverflow.com/a/34689293/1245231

petrsyn
la source
1
I am using this method too. Think it’s the best because there is no fiddling with constraints. It’s also very compact.
bio
3

Your solution using systemLayoutSizeFittingSize: works if the header view is just updated once on each view appearance. In my case, the header view updated multiple times to reflect status changes. But systemLayoutSizeFittingSize: always reported the same size. That is, the size corresponding to the first update.

To get systemLayoutSizeFittingSize: to return the correct size after each update I had to first remove the table header view before updating it and re-adding it:

self.listTableView.tableHeaderView = nil;
[self.headerView removeFromSuperview];
Kim André Sand
la source
2

This worked for me on ios10 and Xcode 8

func layoutTableHeaderView() {

    guard let headerView = tableView.tableHeaderView else { return }
    headerView.translatesAutoresizingMaskIntoConstraints = false

    let headerWidth = headerView.bounds.size.width;
    let temporaryWidthConstraints = NSLayoutConstraint.constraintsWithVisualFormat("[headerView(width)]", options: NSLayoutFormatOptions(rawValue: UInt(0)), metrics: ["width": headerWidth], views: ["headerView": headerView])

    headerView.addConstraints(temporaryWidthConstraints)

    headerView.setNeedsLayout()
    headerView.layoutIfNeeded()

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

    frame.size.height = height
    headerView.frame = frame

    self.tableView.tableHeaderView = headerView

    headerView.removeConstraints(temporaryWidthConstraints)
    headerView.translatesAutoresizingMaskIntoConstraints = true

}
Ravi Randeria
la source
2

It works for both header view and footer just replace the header with footer

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

        headerView.setNeedsLayout()
        headerView.layoutIfNeeded()

        let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
        var frame = headerView.frame
        frame.size.height = height
        headerView.frame = frame

        tableView.tableHeaderView = headerView
    }
}
Sai kumar Reddy
la source
1

For iOS 12 and above, the following steps will ensure autolayout works properly in both your table and the header.

  1. Create your tableView first, then the header.
  2. At the end of your header creation code, call:
[headerV setNeedsLayout];
[headerV layoutIfNeeded];
  1. Upon orientation change, mark the header for layout again and reload the table, this needs to happen slightly after the orientation change is reported:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 *NSEC_PER_SEC), dispatch_get_main_queue(), ^{

  [tableV.tableHeaderView setNeedsLayout];
  [tableV.tableHeaderView layoutIfNeeded];
  [tableV reloadData];});
RunLoop
la source
0

In my case viewDidLayoutSubviews worked better. viewWillLayoutSubviews causes white lines of a tableView to appear. Also I added checking if my headerView object already exists.

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    if ( ! self.userHeaderView ) {
        // Setup HeaderView
        self.userHeaderView = [[[NSBundle mainBundle] loadNibNamed:@"SSUserHeaderView" owner:self options:nil] objectAtIndex:0];
        [self.userHeaderView setNeedsLayout];
        [self.userHeaderView layoutIfNeeded];
        CGFloat height = [self.userHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect headerFrame = self.userHeaderView.frame;
        headerFrame.size.height = height;
        self.userHeaderView.frame = headerFrame;
        self.tableView.tableHeaderView = self.userHeaderView;

        // Update HeaderView with data
        [self.userHeaderView updateWithProfileData];
    }
}
Denis Kutlubaev
la source
0

It is quite possible to use generic AutoLayout-based UIView with any AL inner subview structure as a tableHeaderView.

The only thing one needs is to set a simple tableFooterView before!

Let self.headerView is some constraint-based UIView.

- (void)viewDidLoad {

    ........................

    self.tableView.tableFooterView = [UIView new];

    [self.headerView layoutIfNeeded]; // to set initial size

    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;

    // and the key constraint
    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:self.headerView.trailingAnchor].active = YES;
}

If self.headerView changes height under UI rotation one have to implement

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition: ^(id<UIViewControllerTransitionCoordinatorContext> context) {
        // needed to resize header height
        self.tableView.tableHeaderView = self.headerView;
    } completion: NULL];
}

One can use ObjC category for this purpose

@interface UITableView (AMHeaderView)
- (void)am_insertHeaderView:(UIView *)headerView;
@end

@implementation UITableView (AMHeaderView)

- (void)am_insertHeaderView:(UIView *)headerView {

    NSAssert(self.tableFooterView, @"Need to define tableFooterView first!");

    [headerView layoutIfNeeded];

    self.tableHeaderView = headerView;

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

    [self.tableFooterView.trailingAnchor constraintEqualToAnchor:headerView.trailingAnchor].active = YES;
}
@end

And also Swift extension

extension UITableView {

    func am_insertHeaderView2(_ headerView: UIView) {

        assert(tableFooterView != nil, "Need to define tableFooterView first!")

        headerView.layoutIfNeeded()

        tableHeaderView = headerView

        leadingAnchor.constraint(equalTo: headerView.leadingAnchor).isActive = true
        trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
        topAnchor.constraint(equalTo: headerView.topAnchor).isActive = true

        tableFooterView?.trailingAnchor.constraint(equalTo: headerView.trailingAnchor).isActive = true
    }
}
malex
la source
0

Copied from this post

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    
    if let headerView = tableView.tableHeaderView {

        let height = headerView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height
        var headerFrame = headerView.frame
        
        //Comparison necessary to avoid infinite loop
        if height != headerFrame.size.height {
            headerFrame.size.height = height
            headerView.frame = headerFrame
            tableView.tableHeaderView = headerView
        }
    }
}
Sourabh Sharma
la source