UITableView的tableHeaderView可以使用AutoLayout吗?

114

自从我发现了AutoLayout,我在任何地方都使用它,现在我正在尝试将其与tableHeaderView一起使用。

我创建了一个UIViewsubclass,添加了所有我想要的东西(标签等),并设置了它们的约束条件,然后我将这个CustomView添加到UITableViewtableHeaderView中。

一切都很正常,除了UITableView总是显示在CustomView的上方,我的意思是CustomViewUITableView下面,所以看不见!

无论我做什么,UITableViewtableHeaderView的高度始终为0(宽度、x和y也是如此)。

我的问题是:是否有可能不用手动设置框架就能完成这个?

编辑: 我使用的CustomViewsubview具有以下约束条件:

_title = [[UILabel alloc]init];
_title.text = @"Title";
[self addSubview:_title];
[_title keep:[KeepTopInset rules:@[[KeepEqual must:5]]]]; // title has to stay at least 5 away from the supperview Top
[_title keep:[KeepRightInset rules:@[[KeepMin must:5]]]];
[_title keep:[KeepLeftInset rules:@[[KeepMin must:5]]]];
[_title keep:[KeepBottomInset rules:@[[KeepMin must:5]]]];

我正在使用一个方便的库“KeepLayout”,因为手动编写约束需要花费太长时间,而单个约束需要太多行,但这些方法是自解释的。

UITableView有这些约束:

_tableView = [[UITableView alloc]init];
_tableView.translatesAutoresizingMaskIntoConstraints = NO;
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.backgroundColor = [UIColor clearColor];
[self.view addSubview:_tableView];
[_tableView keep:[KeepTopInset rules:@[[KeepEqual must:0]]]];// These 4 constraints make the UITableView stays 0 away from the superview top left right and bottom.
[_tableView keep:[KeepLeftInset rules:@[[KeepEqual must:0]]]];
[_tableView keep:[KeepRightInset rules:@[[KeepEqual must:0]]]];
[_tableView keep:[KeepBottomInset rules:@[[KeepEqual must:0]]]];

_detailsView = [[CustomView alloc]init];
_tableView.tableHeaderView = _detailsView;

我不确定是否需要直接在CustomView上设置一些约束,我认为它的高度取决于其中UILabel"title"上的约束。

编辑2: 经过另一番调查,似乎CustomView的高度和宽度已正确计算,但其顶部仍与UITableView的顶部处于同一水平线上,并且它们在滚动时一起移动。


是的,这是可能的。你能展示一下你正在使用的代码吗?不知道你在标题视图上设置了什么限制,很难给出建议。 - jrturton
一个简单的方法是在IB中将该视图添加到tableView中。只需在包含tableview的同一场景中创建视图,然后将其拖动到表格中即可。 - Mariam K.
我尽可能地想避免使用IB,到目前为止我还没有必须使用它的情况,如果我无法使其正常工作,我会尝试使用IB。 - ItsASecret
1
苹果建议开发者在自动布局时尽可能使用IB,这真的有助于避免许多不一致的问题。 - Mariam K.
这个答案非常巧妙地解决了这个问题。https://dev59.com/hFsW5IYBdhLWcg3wuJKd - Elijah
显示剩余3条评论
29个回答

155

我在这里提出并回答了一个类似的问题(链接已保留)。总的来说,我只需添加一次标头并使用它来找到所需的高度。然后可以将该高度应用于标题,并且再次设置标题以反映更改。

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

或者更普遍地说:

override func layoutSubviews() {
    super.layoutSubviews()  
    for view in subviews {
        guard let label = view as? UILabel where label.numberOfLines == 0 else { continue }
        label.preferredMaxLayoutWidth = CGRectGetWidth(label.frame)
    }
}

2015年1月更新

不幸的是,这仍然是必要的。下面是布局过程的Swift版本:

tableView.tableHeaderView = header
header.setNeedsLayout()
header.layoutIfNeeded()
header.frame.size = header.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
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
        self.tableHeaderView?.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            header.widthAnchor.constraint(equalTo: self.widthAnchor)
        ])
        header.setNeedsLayout()
        header.layoutIfNeeded()
        header.frame.size =  header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        self.tableHeaderView = header
    }
}

使用方法:

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

8
使用 preferredMaxLayoutWidth 的另一种选择是在使用 systemLayoutSizeFittingSize: 之前,在表格视图的页眉视图上添加一个宽度约束(等于表格视图的宽度)。请注意,这样做可以使布局更准确。 - Benjohn
2
注意:如果您发现表头在第一个单元格上方,请确保将表头属性重置为 self.tableView.tableHeaderView - Laszlo
29
有时候让我惊讶的是,像这样完全琐碎的事情却可以变得如此棘手。 - TylerJames
6
注意:如果您需要获得与tableView相同的精确宽度,应使用所需的水平优先级获取高度,如下所示: let height = header.systemLayoutSizeFittingSize(CGSizeMake(CGRectGetWidth(self.bounds), 0), withHorizontalFittingPriority: UILayoutPriorityRequired, verticalFittingPriority: UILayoutPriorityFittingSizeLevel).height - JakubKnejzlik
4
只需使用 header.setNeedsLayout() header.layoutIfNeeded() header.frame.size = header.systemLayoutSizeFitting(UILayoutFittingCompressedSize) self.tableHeaderView = header 即可在iOS 10.2上运行。 - Kesong Xie
显示剩余6条评论

26

我无法使用约束条件(在代码中)添加标题视图。如果我给我的视图设置宽度和/或高度约束,就会崩溃并出现以下消息:

 "terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Auto Layout still required after executing -layoutSubviews. UITableView's implementation of -layoutSubviews needs to call super."

当我在故事板中给表格视图添加一个视图时,它不显示任何约束条件,并且作为标题视图正常工作,因此我认为标题视图的放置没有使用约束条件。它在这方面似乎不像普通视图那样运行。

宽度自动设置为表格视图的宽度,唯一需要设置的是高度--原点值被忽略,所以你放入任何值都无关紧要。例如,这个也可以正常工作(rect的值为0,0,0,80):

UIView *headerview = [[UIView alloc] initWithFrame:CGRectMake(1000,1000, 0, 80)];
headerview.backgroundColor = [UIColor yellowColor];
self.tableView.tableHeaderView = headerview;

我也遇到了这个异常,但是将一个类别添加到UITableView中就解决了。我在这个答案中找到了它:https://dev59.com/XWcs5IYBdhLWcg3wu2jS#12941256 - ItsASecret
我仍然会尝试您建议的内容,但是现在已经凌晨1点34分了,我要去睡觉了。非常感谢您花时间回答!(但我真的不想指定高度,我希望它可以由我在自定义视图标签上设置的约束计算出来。) - ItsASecret
我已经尝试过了,设置框架是可以的,但我正在寻找一种避免设置框架的方法,我会继续寻找,如果找不到其他方法,我会接受你的答案。 - ItsASecret
1
如果添加的表头视图具有 translatesAutoresizingMaskIntoConstraints = NO,我会在测试 7.1 时遇到此异常。打开翻译可以防止出错 - 我怀疑 UITableView 在 7.1 中不会尝试自动布局其表头视图,并且需要预先设置框架的内容。 - Benjohn

19

我在这里看到了很多方法做了很多不必要的事情,但是你在使用头视图的自动布局时并不需要那么多。你只需要创建你的xib文件,放置你的约束条件并像这样实例化它:

func loadHeaderView () {
        guard let headerView = Bundle.main.loadNibNamed("CourseSearchHeader", owner: self, options: nil)?[0] as? UIView else {
            return
        }
        headerView.autoresizingMask = .flexibleWidth
        headerView.translatesAutoresizingMaskIntoConstraints = true
        tableView.tableHeaderView = headerView
    }

这对我们在 iOS 11 上使用具有多行标签的动态高度标题也起作用。 - Ben Scheirman
1
当然,您也可以在IB中删除flexibleHeight-Autoresizing-Option。 - d4Rk
我一直在尝试设置我的tableFooterView的高度(通过xib / nib),但是无法成功设置frame,height,layoutIfNeeded()等。但是这个解决方案最终让我成功设置了它。 - vikzilla
在 xib 文件中,不要忘记将高度约束设置为整个视图。 - Denis Kutlubaev

7
另一种解决方法是将标题视图的创建调度到下一个主线程调用中:
- (void)viewDidLoad {
    [super viewDidLoad];

    // ....

    dispatch_async(dispatch_get_main_queue(), ^{
        _profileView = [[MyView alloc] initWithNib:@"MyView.xib"];
        self.tableView.tableHeaderView = self.profileView;
    });
}

注意:修复了加载的视图具有固定高度时出现的错误。当标题高度仅取决于其内容时,我还没有尝试过。
编辑:
您可以通过实现此function并在viewDidLayoutSubviews中调用它来找到更清晰的解决方案。
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    [self sizeHeaderToFit];
}

1
@TussLaszlo tableHeaderView在自动布局中有些问题。虽然有一些解决方法,像这个。但是自从我写了这段代码以来,在 https://dev59.com/HGMk5IYBdhLWcg3wvQcU#21099430 找到了一个更好、更清晰的解决方案,只需调用其 - (void)sizeHeaderToFit 即可在 viewDidLayoutSubviews 里实现。 - Martin

5
你可以使用systemLayoutSizeFittingSize方法让自动布局为你提供尺寸。
然后,你可以使用这个尺寸来创建应用程序的框架。每当你需要知道使用内部自动布局的视图的大小时,这种技术都适用。
在Swift中的代码如下:
//Create the view
let tableHeaderView = CustomTableHeaderView()

//Set the content
tableHeaderView.textLabel.text = @"Hello world"

//Ask auto layout for the smallest size that fits my constraints    
let size = tableHeaderView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)

//Create a frame    
tableHeaderView.frame = CGRect(origin: CGPoint.zeroPoint, size: size)

//Set the view as the header    
self.tableView.tableHeaderView = self.tableHeaderView

或者使用Objective-C。
//Create the view
CustomTableHeaderView *header = [[CustomTableHeaderView alloc] initWithFrame:CGRectZero];

//Set the content
header.textLabel.text = @"Hello world";

//Ask auto layout for the smallest size that fits my constraints
CGSize size = [header systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

//Create a frame
header.frame = CGRectMake(0,0,size.width,size.height);

//Set the view as the header  
self.tableView.tableHeaderView = header

需要注意的是,在这种特定情况下,在您的子类中覆盖requiresConstraintBasedLayout会导致执行布局传递,但忽略此布局传递的结果,并将系统框架设置为tableView的宽度和0的高度。


5

将此解决方案扩展为适用于表格页脚视图的 http://collindonnell.com/2015/09/29/dynamically-sized-table-view-header-or-footer-using-auto-layout/

@interface AutolayoutTableView : UITableView

@end

@implementation AutolayoutTableView

- (void)layoutSubviews {
    [super layoutSubviews];

    // Dynamic sizing for the header view
    if (self.tableHeaderView) {
        CGFloat height = [self.tableHeaderView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect headerFrame = self.tableHeaderView.frame;

        // If we don't have this check, viewDidLayoutSubviews() will get
        // repeatedly, causing the app to hang.
        if (height != headerFrame.size.height) {
            headerFrame.size.height = height;
            self.tableHeaderView.frame = headerFrame;
            self.tableHeaderView = self.tableHeaderView;
        }

        [self.tableHeaderView layoutIfNeeded];
    }

    // Dynamic sizing for the footer view
    if (self.tableFooterView) {
        CGFloat height = [self.tableFooterView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        CGRect footerFrame = self.tableFooterView.frame;

        // If we don't have this check, viewDidLayoutSubviews() will get
        // repeatedly, causing the app to hang.
        if (height != footerFrame.size.height) {
            footerFrame.size.height = height;
            self.tableFooterView.frame = footerFrame;
            self.tableFooterView = self.tableFooterView;
        }

        self.tableFooterView.transform = CGAffineTransformMakeTranslation(0, self.contentSize.height - footerFrame.size.height);
        [self.tableFooterView layoutIfNeeded];
    }
}

@end

昨天花了一整天的时间尝试让表头自动调整大小/正确布局。这个解决方案对我很有效。非常感谢。 - docchang
你好!能否请您解释一下 self.tableFooterView.transform 部分是什么意思?为什么它是必要的? - marvin_yorke
@mrvn transform 用于将页脚移动到 tableView 的底部。 - k06a

5

代码:

  extension UITableView {

          func sizeHeaderToFit(preferredWidth: CGFloat) {
            guard let headerView = self.tableHeaderView else {
              return
            }

            headerView.translatesAutoresizingMaskIntoConstraints = false
            let layout = NSLayoutConstraint(
              item: headerView,
              attribute: .Width,
              relatedBy: .Equal,
              toItem: nil,
              attribute:
              .NotAnAttribute,
              multiplier: 1,
              constant: preferredWidth)

            headerView.addConstraint(layout)

            let height = headerView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
            headerView.frame = CGRectMake(0, 0, preferredWidth, height)

            headerView.removeConstraint(layout)
            headerView.translatesAutoresizingMaskIntoConstraints = true

            self.tableHeaderView = headerView
          }
  }

它能够正常工作。给所有tableHeaderview的子视图设置适当的自动布局约束。如果您错过了一个约束,它将无法正常工作。 - abhimuralidharan

5

更新至Swift 4.2

extension UITableView {

    var autolayoutTableViewHeader: UIView? {
        set {
            self.tableHeaderView = newValue
            guard let header = newValue else { return }
            header.setNeedsLayout()
            header.layoutIfNeeded()
            header.frame.size = 
            header.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
            self.tableHeaderView = header
        }
        get {
            return self.tableHeaderView
        }
    }
}

4
奇怪的事情发生了。 systemLayoutSizeFittingSize 在iOS9中运作良好,但在我的情况下,在iOS8中却不起作用。因此,这个问题很容易解决。只需在标题中获取到底部视图的链接,并在viewDidLayoutSubviews调用super之后通过插入高度来更新标题视图边界,如CGRectGetMaxY(yourview.frame) + padding。
更新:最简单的解决方案: 因此,在标题视图中放置子视图并将其固定到左侧、右侧和顶部。在该子视图中,使用自动高度约束放置子视图。之后,把所有工作都交给自动布局(无需计算)。
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];

    CGFloat height = CGRectGetMaxY(self.tableView.tableHeaderView.subviews.firstObject.frame);
    self.tableView.tableHeaderView.bounds = CGRectMake(0, 0, CGRectGetWidth(self.tableView.bounds), height);
    self.tableView.tableHeaderView = self.tableView.tableHeaderView;
}

作为结果,子视图像应该一样进行扩展或收缩,最后它调用viewDidLayoutSubviews。此时,我们知道视图的实际大小,因此通过重新分配来设置headerView的高度并更新它。 非常有效!
对于底部视图也同样有效。

1
这在iOS 10中对我来说是一个循环。 - Simon

4
以下是我成功的做法:
  1. 使用一个普通的 UIView 作为页眉视图。
  2. 向该 UIView 添加子视图。
  3. 在子视图上使用自动布局。
我认为主要的好处是限制框架计算。苹果应该真正更新 UITableView 的 API,使这更容易。
使用 SnapKit 的示例:
let layoutView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 60))
layoutView.backgroundColor = tableView.backgroundColor
tableView.tableHeaderView = layoutView

let label = UILabel()
layoutView.addSubview(label)
label.text = "I'm the view you really care about"
label.snp_makeConstraints { make in
    make.edges.equalTo(EdgeInsets(top: 10, left: 15, bottom: -5, right: -15))
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接