在 tableHeaderView 中使用自动布局
Using autolayout in a tableHeaderView
我有一个包含 multi-line UILabel
的 UIView
子类。此视图使用自动布局。
我想将此视图设置为 UITableView
的 tableHeaderView
( 而不是 部分 header)。此 header 的高度将取决于标签的文本,而标签的文本又取决于设备的宽度。那种场景自动布局应该很擅长。
我已经找到并尝试 many many solutions 使它正常工作,但无济于事。我尝试过的一些事情:
- 在
layoutSubviews
期间在每个标签上设置一个 preferredMaxLayoutWidth
- 定义一个
intrinsicContentSize
- 正在尝试确定视图所需的大小并手动设置
tableHeaderView
的框架。
- 设置 header 时向视图添加宽度约束
- 一堆其他的东西
我遇到的一些各种故障:
- 标签超出视图的宽度,不换行
- 框架的高度为 0
- 应用程序崩溃并出现异常
Auto Layout still required after executing -layoutSubviews
该解决方案(或多个解决方案,如有必要)应该适用于 iOS 7 和 iOS 8。请注意,所有这些都是以编程方式完成的。我已经设置了一个 small sample project 以防你想破解它以查看问题。我已将我的努力重新设置为以下起点:
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;
我错过了什么?
你的约束有点偏离。看看这个,如果您有任何问题,请告诉我。出于某种原因,我很难让视图的背景保持红色?因此,我创建了一个填充视图,用于填充因 titleLabel
和 subtitleLabel
高度大于 imageView
高度而产生的空隙
- (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;
}
到目前为止,我自己的最佳答案涉及设置 tableHeaderView
一次并强制进行布局传递。这允许测量所需的尺寸,然后我用它来设置 header 的框架。而且,与 tableHeaderView
s 一样,我必须再次设置它以应用更改。
- (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 月更新
不幸的是,这似乎仍然是必要的。这是布局过程的 swift 版本:
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)
以下 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
"strange" 步骤的说明:
首先我们将 headerView 的宽度与 tableView 的宽度联系起来:它有助于旋转并防止 headerView 的以 X 为中心的子视图的深度左移。
(the Magic!) 我们在headerView中插入假的layoutView:
此时此刻,我们强烈需要删除所有 headerView 约束,
将 layoutView 扩展到 headerView,然后恢复初始 headerView
约束。 碰巧约束的顺序有一定的意义!
在我们获得正确的 headerView 高度自动计算的方式中,也正确
所有 headerView 子视图的 X 中心化。
那么我们只需要重新布局headerView就可以得到正确的tableView
高度计算和 headerView 定位上面的部分没有
相交.
P.S。它也适用于 iOS8。一般情况下这里是不可能注释掉任何代码串的。
使用 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))
}
}
对于仍在寻找解决方案的任何人,这是针对 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 视图单元格相同的方式管理 tableHeaderView
的框架。这是通过 tableView
的 beginUpdates/endUpdates
.
完成的
问题是 tableView
在更新 children 帧时不关心 AutoLayout。它使用 current tableHeaderView
的 size 来确定第一个 cell/section header 应该在哪里。
1) 添加宽度约束,以便 tableHeaderView
在我们调用 layoutIfNeeded() 时使用此宽度。还要添加 centerX 和 top 约束以相对于 tableView
.
正确定位它
2) 让tableView
知道tableHeaderView
的最新大小,例如,当设备旋转时,在viewDidLayoutSubviews 中我们可以在tableHeaderView
上调用layoutIfNeeded()。然后,如果大小改变了,调用beginUpdates/endUpdates.
请注意,我没有在一个函数中包含 beginUpdates/endUpdates,因为我们可能希望稍后调用。
这里的一些答案帮助我非常接近我所需要的。但是当我在纵向和横向之间旋转设备 back-and-forth 时,我遇到了与系统设置的约束 "UIView-Encapsulated-Layout-Width" 的冲突。我下面的解决方案主要基于 marcoarment 的这个要点(归功于他):https://gist.github.com/marcoarment/1105553afba6b4900c10。该解决方案不依赖于包含 UILabel 的 header 视图。共有 3 个部分:
- 在 UITableView 的扩展中定义的函数。
- 从视图控制器的 viewWillAppear() 调用函数。
- 从视图控制器的 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
加载到 Header
,您实际上应该使用 viewForHeaderInSection
委托方法。您可以为您的 header (UIView
nib) 定制一个 Nib
文件。 Nib
必须有一个控制器 class 子class 是 UITableViewHeaderFooterView
喜欢-
class YourCustomHeader: UITableViewHeaderFooterView {
//@IBOutlets, delegation and other methods as per your needs
}
确保您的 Nib 文件名与 class 名称相同,这样您就不会混淆并且更易于管理。像 YourCustomHeader.xib
和 YourCustomHeader.swift
(包含 class YourCustomHeader
)。然后,只需使用界面生成器中的身份检查器将 YourCustomHeader
分配给您的 Nib 文件。
然后在主视图控制器的 viewDidLoad
中将 Nib
文件注册为您的 header 视图-
tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader")
然后在您的 heightForHeaderInSection
中只是 return 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
}
如果不采用已接受答案中建议的 "Hackish" 方式,这是一种更简单、更合适的方法,因为多个强制布局可能会影响您应用的性能,尤其是当您有多个自定义布局时 header在你的表格视图中。按照我的建议执行上述方法后,您会注意到 Header
(和或 Footer
)视图根据自定义视图的内容大小神奇地扩展和收缩(前提是您在自定义视图中使用 AutoLayout , 即 YourCustomHeader
, nib 文件).
这应该可以解决使用 AutoLayout 的 UITableView 的 headerView 或 footerView 的问题。
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
var tableFooterViewWithAutolayout: UIView? {
set (view) {
tableFooterView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableFooterView = view
}
}
get {
return tableFooterView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}
我有一个包含 multi-line UILabel
的 UIView
子类。此视图使用自动布局。
我想将此视图设置为 UITableView
的 tableHeaderView
( 而不是 部分 header)。此 header 的高度将取决于标签的文本,而标签的文本又取决于设备的宽度。那种场景自动布局应该很擅长。
我已经找到并尝试 many many solutions 使它正常工作,但无济于事。我尝试过的一些事情:
- 在
layoutSubviews
期间在每个标签上设置一个 - 定义一个
intrinsicContentSize
- 正在尝试确定视图所需的大小并手动设置
tableHeaderView
的框架。 - 设置 header 时向视图添加宽度约束
- 一堆其他的东西
preferredMaxLayoutWidth
我遇到的一些各种故障:
- 标签超出视图的宽度,不换行
- 框架的高度为 0
- 应用程序崩溃并出现异常
Auto Layout still required after executing -layoutSubviews
该解决方案(或多个解决方案,如有必要)应该适用于 iOS 7 和 iOS 8。请注意,所有这些都是以编程方式完成的。我已经设置了一个 small sample project 以防你想破解它以查看问题。我已将我的努力重新设置为以下起点:
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;
我错过了什么?
你的约束有点偏离。看看这个,如果您有任何问题,请告诉我。出于某种原因,我很难让视图的背景保持红色?因此,我创建了一个填充视图,用于填充因 titleLabel
和 subtitleLabel
高度大于 imageView
- (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;
}
到目前为止,我自己的最佳答案涉及设置 tableHeaderView
一次并强制进行布局传递。这允许测量所需的尺寸,然后我用它来设置 header 的框架。而且,与 tableHeaderView
s 一样,我必须再次设置它以应用更改。
- (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 月更新
不幸的是,这似乎仍然是必要的。这是布局过程的 swift 版本:
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)
以下 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
"strange" 步骤的说明:
首先我们将 headerView 的宽度与 tableView 的宽度联系起来:它有助于旋转并防止 headerView 的以 X 为中心的子视图的深度左移。
(the Magic!) 我们在headerView中插入假的layoutView: 此时此刻,我们强烈需要删除所有 headerView 约束, 将 layoutView 扩展到 headerView,然后恢复初始 headerView 约束。 碰巧约束的顺序有一定的意义! 在我们获得正确的 headerView 高度自动计算的方式中,也正确
所有 headerView 子视图的 X 中心化。那么我们只需要重新布局headerView就可以得到正确的tableView
高度计算和 headerView 定位上面的部分没有 相交.
P.S。它也适用于 iOS8。一般情况下这里是不可能注释掉任何代码串的。
使用 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))
}
}
对于仍在寻找解决方案的任何人,这是针对 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 视图单元格相同的方式管理 tableHeaderView
的框架。这是通过 tableView
的 beginUpdates/endUpdates
.
问题是 tableView
在更新 children 帧时不关心 AutoLayout。它使用 current tableHeaderView
的 size 来确定第一个 cell/section header 应该在哪里。
1) 添加宽度约束,以便 tableHeaderView
在我们调用 layoutIfNeeded() 时使用此宽度。还要添加 centerX 和 top 约束以相对于 tableView
.
2) 让tableView
知道tableHeaderView
的最新大小,例如,当设备旋转时,在viewDidLayoutSubviews 中我们可以在tableHeaderView
上调用layoutIfNeeded()。然后,如果大小改变了,调用beginUpdates/endUpdates.
请注意,我没有在一个函数中包含 beginUpdates/endUpdates,因为我们可能希望稍后调用。
这里的一些答案帮助我非常接近我所需要的。但是当我在纵向和横向之间旋转设备 back-and-forth 时,我遇到了与系统设置的约束 "UIView-Encapsulated-Layout-Width" 的冲突。我下面的解决方案主要基于 marcoarment 的这个要点(归功于他):https://gist.github.com/marcoarment/1105553afba6b4900c10。该解决方案不依赖于包含 UILabel 的 header 视图。共有 3 个部分:
- 在 UITableView 的扩展中定义的函数。
- 从视图控制器的 viewWillAppear() 调用函数。
- 从视图控制器的 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
加载到 Header
,您实际上应该使用 viewForHeaderInSection
委托方法。您可以为您的 header (UIView
nib) 定制一个 Nib
文件。 Nib
必须有一个控制器 class 子class 是 UITableViewHeaderFooterView
喜欢-
class YourCustomHeader: UITableViewHeaderFooterView {
//@IBOutlets, delegation and other methods as per your needs
}
确保您的 Nib 文件名与 class 名称相同,这样您就不会混淆并且更易于管理。像 YourCustomHeader.xib
和 YourCustomHeader.swift
(包含 class YourCustomHeader
)。然后,只需使用界面生成器中的身份检查器将 YourCustomHeader
分配给您的 Nib 文件。
然后在主视图控制器的 viewDidLoad
中将 Nib
文件注册为您的 header 视图-
tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader")
然后在您的 heightForHeaderInSection
中只是 return 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
}
如果不采用已接受答案中建议的 "Hackish" 方式,这是一种更简单、更合适的方法,因为多个强制布局可能会影响您应用的性能,尤其是当您有多个自定义布局时 header在你的表格视图中。按照我的建议执行上述方法后,您会注意到 Header
(和或 Footer
)视图根据自定义视图的内容大小神奇地扩展和收缩(前提是您在自定义视图中使用 AutoLayout , 即 YourCustomHeader
, nib 文件).
这应该可以解决使用 AutoLayout 的 UITableView 的 headerView 或 footerView 的问题。
extension UITableView {
var tableHeaderViewWithAutolayout: UIView? {
set (view) {
tableHeaderView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableHeaderView = view
}
}
get {
return tableHeaderView
}
}
var tableFooterViewWithAutolayout: UIView? {
set (view) {
tableFooterView = view
if let view = view {
lowerPriorities(view)
view.frameSize = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
tableFooterView = view
}
}
get {
return tableFooterView
}
}
fileprivate func lowerPriorities(_ view: UIView) {
for cons in view.constraints {
if cons.priority.rawValue == 1000 {
cons.priority = UILayoutPriority(rawValue: 999)
}
for v in view.subviews {
lowerPriorities(v)
}
}
}
}