如何使用自动布局设置 tableHeaderView (UITableView)的高度?

过去的三四个小时里,我一直用这个把头撞在墙上,但我似乎想不明白。我有一个 UIViewController,里面有一个全屏的 UITableView (屏幕上还有其他一些东西,这就是为什么我不能使用 UITableViewController) ,我想让我的 tableHeaderView 自动调整大小。不用说,这不是合作。

见下面的截图。

enter image description here

因为 overviewLabel (例如“列表概述信息在这里。”(text)有动态内容,我使用自动布局来调整它的大小和它的视图。我已经很好地调整了所有内容的大小,除了 tableHeaderView,它位于层次结构中视差表视图的正下方。

我找到的调整头部视图大小的唯一方法是通过编程方式,代码如下:

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

在本例中,headerFrameHeight 是对 tableViewHeader 高度的手动计算,如下所示 (innerHeaderView 是白色区域,或者第二个“视图”,headerView 是 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);

手工计算是可行的,但是如果将来情况发生变化,它就会变得笨拙并且容易出错。我希望能够让 tableHeaderView 根据提供的约束自动调整大小,就像在其他任何地方一样。但我怎么也想不明白。

关于这个问题有几篇文章,但是没有一篇是清楚的,最后让我更加困惑:

将 translatesAutoresizingMaskIntoConstraint 属性更改为 NO 实际上没有意义,因为这只会导致错误,而且在概念上也没有意义。

如果你能帮忙,我会很感激的!

编辑1: 多亏汤姆斯威夫特的建议,我终于弄明白了。不需要手动计算概述的高度,我可以按照以下方式计算它,然后像前面一样重新设置 tableHeaderView。

[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: 正如其他人所指出的,Edit 1中发布的解决方案似乎不能在 viewDidLoad 中工作。但是,它似乎在 viewWillLayoutSubviews 中工作。下面的示例代码:

// 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;
}
69708 次浏览

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?

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

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

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

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


}

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

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

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

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

This solution works perfectly for me:

https://spin.atomicobject.com/2017/08/11/swift-extending-uitableviewcontroller/

It extends the UITableViewController. But if you are just using a UITableView, it will still work, just extend the UITableView instead of the UITableViewController. Call the methods sizeHeaderToFit() or sizeFooterToFit() whenever there is an event that changes the tableViewHeader height.

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

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