ios 在tableHeaderView中使用自动布局

chhkpiq4  于 2023-06-25  发布在  iOS
关注(0)|答案(9)|浏览(182)

bounty将在5天内到期。此问题的答案有资格获得+500声望奖励。Fattie希望引起更多关注这个问题。

我有一个UIView子类,其中包含一个多行UILabel。此视图使用自动布局。

我想将这个视图设置为UITableViewtableHeaderView(* 不是 * 一个节头)。此标题的高度将取决于标签的文本,而标签的文本又取决于设备的宽度。这种场景自动布局应该是很棒的。
我已经找到并尝试manymanysolutions让这个工作,但无济于事。我试过的一些方法:

  • layoutSubviews期间,在每个标签上设置preferredMaxLayoutWidth
  • 定义intrinsicContentSize
  • 试图弄清楚视图所需的大小,并手动设置tableHeaderView的帧。
  • 在设置页眉时向视图添加宽度约束
  • 一堆其他的东西

以下是我遇到的各种失败:

  • 标签延伸超出视图宽度,不换行
  • 框架高度为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;

我错过了什么?

x759pob2

x759pob21#

到目前为止,我自己最好的答案是设置tableHeaderView一次,然后强制执行布局。这允许测量所需的大小,然后使用该大小设置头部的帧。而且,和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)
7rfyedvj

7rfyedvj2#

对于任何仍在寻找解决方案的人来说,这是针对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管理tableHeaderView的框架,就像表格视图单元格一样。这是通过tableViewbeginUpdates/endUpdates完成的。

问题是tableView在更新子帧时并不关心AutoLayout。它使用 * current * tableHeaderView的 * size * 来确定第一个单元格/节标题应该在哪里。
1)添加一个宽度约束,以便每当我们调用layoutIfNeeded()时,tableHeaderView都使用此宽度。另外,添加centerX和top约束,以相对于tableView正确定位。
2)为了让tableView知道tableHeaderView的最新大小,例如,当设备旋转时,我们可以在viewDidLayoutSubviews中调用tableHeaderView的layoutIfNeeded()。然后,如果大小改变了,请调用beginUpdates/endUpdates。

  • 请注意,我没有在一个函数中包含beginUpdates/endUpdates,因为我们可能希望将调用推迟到以后。*

Check out a sample project

q9rjltbz

q9rjltbz3#

以下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的X中心子视图的深度左移。
1.(魔法!)* )我们在headerView中插入假layoutView:此时,我们强烈需要删除所有headerView约束,将layoutView展开为headerView,然后恢复初始headerView约束。* 约束的顺序有一定的意义!* 在我们得到正确的headerView高度自动计算的方式,也正确
所有headerView子视图的X集中化。
1.然后我们只需要重新布局headerView就可以得到正确的tableView
高度计算和页眉查看在截面上方的定位,不相交。

  • P.S.它也适用于iOS8。在常见情况下,不可能注解掉任何代码字符串。*
qlzsbp2j

qlzsbp2j4#

这里的一些答案帮助我非常接近我需要的东西。但是,当在纵向和横向之间来回旋转设备时,我遇到了与系统设置的约束“UIView-Encapsulated-Layout-Width”冲突。我下面的解决方案主要基于marcoarment的这个要点(归功于他):https://gist.github.com/marcoarment/1105553afba6b4900c10。该解决方案不依赖于包含UILabel的头视图。有3个部分:
1.在UITableView的扩展中定义的函数。
1.从视图控制器的viewWillAppear()调用函数。
1.从视图控制器的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)
}
zxlwwiss

zxlwwiss5#

对于使用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)
      }
    }
  }
}
hec6srdp

hec6srdp6#

在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))
    }
}
u2nhd7ah

u2nhd7ah7#

其实就是这么简单:

序言。请注意,tableFooterViewtableHeaderView绝对***与表视图中的 * 节 * 页脚/表头无关。它们在任何方面都不相似、相关或关联***。
此外,在UIKit中,“表”和“节”装饰的范式、处理和视图循环处理完全不同,因此整个问题完全混乱。再加上这里在“2”中解释的完全奇怪的问题(这完全不同于UIKit中的任何其他约束处理),人们可以看到关于表尾的大量混乱是如何出现的。
压倒性的秘密是这样的:

1. xib的垂直优先级必须<1000。

在xib文件中,垂直约束链必须小于1000。这就是全部。以通常的方式加载xib文件。

class DemoFooter: UIIView {
    class func build() -> UIView {
        return UINib(nibName: "DemoFooter", bundle: nil)
         .instantiate(withOwner: nil, options: nil)[0] as! UIView
    }
}

这是让表格页眉/页脚工作的压倒性“秘密”。

2.由于Apple问题,您必须“重置”tableFooterView。事情就是这样试图解决这个苹果问题是完全没有意义的。

你会期望代码看起来像这样

tableFooterView.frame = .. the size

简单的事实是你不能这样做,你必须这样做:

temp = tableFooterView
temp.frame = .. the size
tableFooterView = temp

讨论这是一个苹果错误,软错误,愚蠢,奇怪的行为,错误,或任何其他完全没有意义。事实上,您必须“重置”tableFooterView。
(In你的代码,只需尝试另一种方式,你会发现,一开始页脚只是“浮动在屏幕中间”。在视图周期中的哪个地方尝试绝对没有区别,浪费时间来尝试,如你所愿!这只是一个典型的UIKit f 'up。

3.注意网上看到的 * 不正确 * 查看周期示例代码。

override func viewDidLoad() {
    
    super.viewDidLoad()
    tableView.tableFooterView = DemoFooter.build()
}

override func viewWillLayoutSubviews() {
    
    if let f = tableView.tableFooterView {
        f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        tableView.tableFooterView = f
        print("Recall, you MUST HAVE <1000 priority vert in your xib.")
    }
    
    super.viewWillLayoutSubviews()
}

你在viewWillLayoutSubviews而不是***周期的后期执行。
你***不要
“检查高度是否改变”,就像在网上的许多例子中看到的。这显然是令人难以置信的毫无意义/非常糟糕的做法,原因不值得在这里。
为了清楚起见,如果它是一个简单的静态视图,你可以这样做以避免输入“viewDidLoad”:

override func viewWillLayoutSubviews() {
    
    let f = QuickFoot.build()
    f.frame.size = f.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
    tableView.tableFooterView = f
    
    super.viewWillLayoutSubviews()
}

4、复核点(1)。

或者,简单地说,什么都不起作用(您将在控制台中得到错误垃圾邮件)。

csga3l58

csga3l588#

你的限制有点偏离了。看看这个,如果你有什么问题告诉我。出于某种原因,我很难让背景的视图保持红色?因此,我创建了一个填充视图,该视图填充了titleLabelsubtitleLabel的高度大于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;
}
6rvt4ljy

6rvt4ljy9#

我会加上我的2美分,因为这个问题是高度索引在谷歌。我觉得你应该用

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

你的ViewDidLoad此外,要将自定义UIView加载到Header中,实际上应该使用viewForHeaderInSection委托方法。你可以有一个自定义的Nib文件作为你的头(UIView笔尖)。该Nib必须有一个控制器类,该控制器类子类UITableViewHeaderFooterView,如-

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

请确保您的Nib文件名与类名相同,这样您就不会感到困惑,并且更容易管理。例如YourCustomHeader.xibYourCustomHeader.swift(包含class YourCustomHeader)。然后,只需在界面构建器中使用身份检查器将YourCustomHeader分配给您的Nib文件。
然后在主视图控制器的viewDidLoad中注册Nib文件作为头视图-

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。一旦你按照我的建议执行了上面的方法,你会注意到你的Header(和/或Footer)视图会根据你的自定义视图的内容大小神奇地展开和收缩(假设你在自定义视图中使用AutoLayout,即YourCustomHeader,nib文件)。

相关问题