在tableHeaderView中使用自动布局

我有一个包含多行UILabelUIView子类。 该视图使用自动布局。

在这里输入图像说明

我想将此视图设置为UITableView而不是节标题)的tableHeaderView 。 该标题的高度将取决于标签的文字,而标签的文字又取决于设备的宽度。 这种场景自动布局应该是伟大的。

我发现并尝试了很多 很多的 解决scheme来获得这个工作,但无济于事。 我尝试过的一些事情:

  • layoutSubviews期间在每个标签上设置preferredMaxLayoutWidth
  • 定义一个intrinsicContentSize
  • 试图找出所需的视图大小,并手动设置tableHeaderView的框架。
  • 在设置标题时为视图添加宽度约束
  • 一堆其他的东西

我遇到的一些各种失败:

  • 标签延伸超出视图的宽度,不包裹
  • 帧的高度是0
  • 应用程序崩溃, Auto Layout still required after executing -layoutSubviews

解决scheme(或者解决scheme,如果需要的话)应该适用于iOS 7和iOS 8.请注意,所有这些都是以编程方式完成的。 我已经build立了一个小样本项目 ,以防你想破解这个问题。 我已将我的努力重新设置到以下的起点:

 SCAMessageView *header = [[SCAMessageView alloc] init]; header.titleLabel.text = @"Warning"; 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."; self.tableView.tableHeaderView = header; 

我错过了什么?

到目前为止,我自己的最佳答案涉及到设置tableHeaderView一次,并强制布局传递。 这允许测量所需的大小,然后我使用它来设置标题的框架。 而且,和tableHeaderView ,我必须再次设置它来应用更改。

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

对于多行标签,这也依赖于自定义视图(在这种情况下的消息视图)设置每个的preferredMaxLayoutWidth

 - (void)layoutSubviews { [super layoutSubviews]; self.titleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.titleLabel.frame); self.subtitleLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.subtitleLabel.frame); } 

2015年1月更新

不幸的是,这仍然是必要的 这是一个布局过程的快速版本:

 tableView.tableHeaderView = header header.setNeedsLayout() header.layoutIfNeeded() let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height var frame = header.frame frame.size.height = height header.frame = frame tableView.tableHeaderView = header 

我发现将它移动到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 header.setNeedsLayout() header.layoutIfNeeded() let height = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height var frame = header.frame frame.size.height = height header.frame = frame self.tableHeaderView = header } } 

用法:

 let header = SCAMessageView() header.titleLabel.text = "Warning" header.subtitleLabel.text = "Warning message here." tableView.setAndLayoutTableHeaderView(header) 

对于仍在寻找解决scheme的人来说,这是Swift 3和iOS 9+。 这里只使用AutoLayout。 它也正确更新设备旋转。

 extension UITableView { // 1. func setTableHeaderView(headerView: UIView) { headerView.translatesAutoresizingMaskIntoConstraints = false self.tableHeaderView = headerView // ** Must setup AutoLayout after set tableHeaderView. headerView.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true headerView.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true headerView.topAnchor.constraint(equalTo: self.topAnchor).isActive = true } // 2. func shouldUpdateHeaderViewFrame() -> Bool { guard let headerView = self.tableHeaderView else { return false } let oldSize = headerView.bounds.size // Update the size headerView.layoutIfNeeded() let newSize = headerView.bounds.size return oldSize != newSize } } 

使用:

 override func viewDidLoad() { ... // 1. self.tableView.setTableHeaderView(headerView: customView) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // 2. Reflect the latest size in tableHeaderView if self.tableView.shouldUpdateHeaderViewFrame() { // **This is where table view's content (tableHeaderView, section headers, cells) // frames are updated to account for the new table header size. self.tableView.beginUpdates() self.tableView.endUpdates() } } 

要点是你应该让tableView像table view单元一样pipe理tableHeaderView的框架。 这是通过tableViewbeginUpdates/endUpdates

事情是,当它更新子框架时, tableView不关心AutoLayout。 它使用当前 tableHeaderView大小来确定第一个单元格/区段标题的位置。

1)只要调用layoutIfNeeded(),就添加一个宽度约束,以便tableHeaderView使用这个宽度。 还要添加centerX和顶端的约束来正确定位它相对于tableView

2)让tableView知道tableHeaderView的最新大小,例如,当设备旋转时,在viewDidLayoutSubviews中,我们可以调用tableHeaderView上的layoutIfNeeded()。 然后,如果大小已更改,请调用beginUpdates / endUpdates。

请注意,我不在一个函数中包含beginUpdates / endUpdates,因为我们可能希望稍后将该调用延迟。

查看一个示例项目

下面的UITableView扩展解决了tableHeaderView不使用帧的情况下自动播放和定位的所有常见问题:

 @implementation UITableView (AMHeaderView) - (void)am_insertHeaderView:(UIView *)headerView { self.tableHeaderView = headerView; NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem: headerView attribute: NSLayoutAttributeWidth relatedBy: NSLayoutRelationEqual toItem: headerView.superview attribute: NSLayoutAttributeWidth multiplier: 1.0 constant: 0.0]; [headerView.superview addConstraint:constraint]; [headerView layoutIfNeeded]; NSArray *constraints = headerView.constraints; [headerView removeConstraints:constraints]; UIView *layoutView = [UIView new]; layoutView.translatesAutoresizingMaskIntoConstraints = NO; [headerView insertSubview:layoutView atIndex:0]; [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]]; [headerView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:@{@"view": layoutView}]]; [headerView addConstraints:constraints]; self.tableHeaderView = headerView; [headerView layoutIfNeeded]; } @end 

“奇怪”步骤的解释:

  1. 首先,我们将headerView的宽度和tableView的宽度绑定在一起:它有助于在旋转的情况下进行,并且避免了以headerView为中心的子视图的深度左移。

  2. Magic! )我们在headerView中插入伪布局视图:此时我们强烈需要删除所有的headerView约束,将layoutView扩展到headerView,然后恢复初始的headerView约束。 碰巧约束的顺序有一定的意义! 在我们得到正确的headerView高度自动计算的方式也是正确的
    所有headerView子视图的X集中。

  3. 那么我们只需要重新布局headerView来获得正确的tableView
    高度计算和headerView定位上面没有相交的部分。

PS它也适用于iOS8。 在这里通常情况下注释掉任何代码string是不可能的。

在Swift 3.0中使用扩展

 extension UITableView { func setTableHeaderView(headerView: UIView?) { // set the headerView tableHeaderView = headerView // check if the passed view is nil guard let headerView = headerView else { return } // check if the tableHeaderView superview view is nil just to avoid // to use the force unwrapping later. In case it fail something really // wrong happened guard let tableHeaderViewSuperview = tableHeaderView?.superview else { assertionFailure("This should not be reached!") return } // force updated layout headerView.setNeedsLayout() headerView.layoutIfNeeded() // set tableHeaderView width tableHeaderViewSuperview.addConstraint(headerView.widthAnchor.constraint(equalTo: tableHeaderViewSuperview.widthAnchor, multiplier: 1.0)) // set tableHeaderView height let height = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height tableHeaderViewSuperview.addConstraint(headerView.heightAnchor.constraint(equalToConstant: height)) } func setTableFooterView(footerView: UIView?) { // set the footerView tableFooterView = footerView // check if the passed view is nil guard let footerView = footerView else { return } // check if the tableFooterView superview view is nil just to avoid // to use the force unwrapping later. In case it fail something really // wrong happened guard let tableFooterViewSuperview = tableFooterView?.superview else { assertionFailure("This should not be reached!") return } // force updated layout footerView.setNeedsLayout() footerView.layoutIfNeeded() // set tableFooterView width tableFooterViewSuperview.addConstraint(footerView.widthAnchor.constraint(equalTo: tableFooterViewSuperview.widthAnchor, multiplier: 1.0)) // set tableFooterView height let height = footerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize).height tableFooterViewSuperview.addConstraint(footerView.heightAnchor.constraint(equalToConstant: height)) } } 

这里的一些答案帮助我非常接近我所需要的。 但是在纵向和横向之间来回旋转设备时,我遇到了系统设置的约束“UIView-Encapsulated-Layout-Width”的冲突。 下面我的解决scheme主要基于marcoarment的这个要点(信贷给他): https ://gist.github.com/marcoarment/1105553afba6b4900c10。 该解决scheme不依赖于包含UILabel的标题视图。 有3个部分:

  1. 在UITableView的扩展中定义的函数。
  2. 从视图控制器的viewWillAppear()调用函数。
  3. 从视图控制器的viewWillTransition()调用函数以处理设备旋转。

UITableView扩展

 func rr_layoutTableHeaderView(width:CGFloat) { // remove headerView from tableHeaderView: guard let headerView = self.tableHeaderView else { return } headerView.removeFromSuperview() self.tableHeaderView = nil // create new superview for headerView (so that autolayout can work): let temporaryContainer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)) temporaryContainer.translatesAutoresizingMaskIntoConstraints = false self.addSubview(temporaryContainer) temporaryContainer.addSubview(headerView) // set width constraint on the headerView and calculate the right size (in particular the height): headerView.translatesAutoresizingMaskIntoConstraints = false let temporaryWidthConstraint = NSLayoutConstraint(item: headerView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 0, constant: width) temporaryWidthConstraint.priority = 999 // necessary to avoid conflict with "UIView-Encapsulated-Layout-Width" headerView.addConstraint(temporaryWidthConstraint) headerView.frame.size = headerView.systemLayoutSizeFitting(UILayoutFittingCompressedSize) // remove the temporary constraint: headerView.removeConstraint(temporaryWidthConstraint) headerView.translatesAutoresizingMaskIntoConstraints = true // put the headerView back into the tableHeaderView: headerView.removeFromSuperview() temporaryContainer.removeFromSuperview() self.tableHeaderView = headerView } 

在UITableViewController中使用

 override func viewDidLoad() { super.viewDidLoad() // build the header view using autolayout: let button = UIButton() let label = UILabel() button.setTitle("Tap here", for: .normal) label.text = "The text in this header will span multiple lines if necessary" label.numberOfLines = 0 let headerView = UIStackView(arrangedSubviews: [button, label]) headerView.axis = .horizontal // assign the header view: self.tableView.tableHeaderView = headerView // continue with other things... } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.tableView.rr_layoutTableHeaderView(width: view.frame.width) } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) self.tableView.rr_layoutTableHeaderView(width: size.width) } 

我会加2美分,因为这个问题在Google中被高度索引。 我想你应该使用

 self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension self.tableView.estimatedSectionHeaderHeight = 200 //a rough estimate, doesn't need to be accurate 

在你的ViewDidLoad 。 此外,要加载一个自定义的UIView viewForHeaderInSection你应该真的使用viewForHeaderInSection委托方法。 你可以有一个自定义的头文件( UIView笔尖)。 那个Nib必须有一个控制器类,它的子类UITableViewHeaderFooterView

 class YourCustomHeader: UITableViewHeaderFooterView { //@IBOutlets, delegation and other methods as per your needs } 

确保您的Nib文件名与类名相同,这样您就不会感到困惑,而且pipe理起来也更容易。 像YourCustomHeader.xibYourCustomHeader.swift (包含class YourCustomHeader )。 然后,只需使用界面生成器中的身份检查器将YourCustomHeader分配给您的Nib文件即可。

然后注册Nib文件作为你的标题视图在主视图控制器的viewDidLoad

 tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader") 

然后在你的heightForHeaderInSection只是返回UITableViewAutomaticDimension 。 代表们应该是这样的 –

 func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "YourCustomHeader") as! YourCustomHeader return headerView } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return UITableViewAutomaticDimension } 

这是一个非常简单和适当的方式,没有在接受的答案中提出的“黑客”方式,因为多个强制布局可能会影响您的应用程序的性能,特别是如果你有多个自定义标题在你的tableview。 一旦你做了上面的方法,我build议你会发现你的Header (和/或Footer )视图根据你的自定义视图的内容大小(假设你在自定义视图中使用AutoLayout,即YourCustomHeader ,nib文件)神奇地扩大和缩小。

你的限制只是一点点。 看看这个,让我知道如果你有任何问题。 由于某种原因,我很难得到保持红色的观点的背景? 所以我创build了一个填充视图,填补了titleLabelsubtitleLabel高度大于titleLabel的高度

 - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.backgroundColor = [UIColor redColor]; self.imageView = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:@"Exclamation"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]]; self.imageView.tintColor = [UIColor whiteColor]; self.imageView.translatesAutoresizingMaskIntoConstraints = NO; self.imageView.backgroundColor = [UIColor redColor]; [self addSubview:self.imageView]; [self.imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self); make.width.height.equalTo(@40); make.top.equalTo(self).offset(0); }]; self.titleLabel = [[UILabel alloc] init]; self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; self.titleLabel.font = [UIFont systemFontOfSize:14]; self.titleLabel.textColor = [UIColor whiteColor]; self.titleLabel.backgroundColor = [UIColor redColor]; [self addSubview:self.titleLabel]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self).offset(0); make.left.equalTo(self.imageView.mas_right).offset(0); make.right.equalTo(self).offset(-10); make.height.equalTo(@15); }]; self.subtitleLabel = [[UILabel alloc] init]; self.subtitleLabel.translatesAutoresizingMaskIntoConstraints = NO; self.subtitleLabel.font = [UIFont systemFontOfSize:13]; self.subtitleLabel.textColor = [UIColor whiteColor]; self.subtitleLabel.numberOfLines = 0; self.subtitleLabel.backgroundColor = [UIColor redColor]; [self addSubview:self.subtitleLabel]; [self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.titleLabel.mas_bottom); make.left.equalTo(self.imageView.mas_right); make.right.equalTo(self).offset(-10); }]; UIView *fillerView = [[UIView alloc] init]; fillerView.backgroundColor = [UIColor redColor]; [self addSubview:fillerView]; [fillerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.imageView.mas_bottom); make.bottom.equalTo(self.subtitleLabel.mas_bottom); make.left.equalTo(self); make.right.equalTo(self.subtitleLabel.mas_left); }]; } return self; }