在tableHeaderView中使用autolayout

时间:2015-01-22 00:48:01

标签: ios uitableview uiview autolayout nslayoutconstraint

我有一个包含多行UIView的{​​{1}}子类。此视图使用autolayout。

enter image description here

我想将此视图设置为UILabel的{​​{1}}(部分标题)。此标题的高度取决于标签的文本,而标签的文本又取决于设备的宽度。那种场景自动布局应该很棒。

我找到并尝试many many solutions让这项工作正常,但无济于事。我试过的一些事情:

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

我遇到的一些各种失败:

  • 标签超出了视图的宽度,不会换行
  • 框架的高度为0
  • 应用程序因异常intrinsicContentSize
  • 而崩溃

解决方案(或解决方案,如有必要)应适用于iOS 7和iOS 8.请注意,所有这些都是以编程方式完成的。我已经设置了small sample project,以防您想要查看问题。我已将我的努力重新设定到以下起点:

tableHeaderView

我错过了什么?

8 个答案:

答案 0 :(得分:20)

到目前为止,我自己的最佳答案包括设置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月更新

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

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)

答案 1 :(得分:5)

对于仍在寻找解决方案的人来说,这适用于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的框架。这是通过tableView的{​​{1}}完成的。

问题是beginUpdates/endUpdates在更新子帧时不关心AutoLayout。它使用当前 tableView size 来确定第一个单元格/节标题的位置。

1)添加宽度约束,以便每当调用layoutIfNeeded()时tableHeaderView都使用此宽度。还要添加centerX和top限制,以便相对于tableHeaderView正确定位。

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

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

Check out a sample project

答案 2 :(得分:2)

以下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为中心的子视图的深度左移。

  2. the Magic!)我们在headerView中插入假layoutView: 此时我们强烈需要删除所有headerView约束, 将layoutView展开到headerView,然后恢复初始headerView 限制。 发生约束的顺序有一定意义! 在我们得到正确的headerView高度自动计算和正确的方式中 所有headerView子视图的X集中化。

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

  4. P.S。它也适用于iOS8。在常见情况下,不可能在这里注释掉任何代码字符串。

答案 3 :(得分:2)

这里的一些答案帮助我非常接近我需要的东西。但是我遇到了与约束的冲突" UIView-Encapsulated-Layout-Width"当在纵向和横向之间来回旋转设备时,由系统设置。我在下面的解决方案很大程度上是基于marcoarment的这个要点(归功于他):https://gist.github.com/marcoarment/1105553afba6b4900c10。该解决方案不依赖于包含UILabel的标题视图。共有3个部分:

  1. UITableView扩展中定义的函数。
  2. 从视图控制器的viewWillAppear()调用该函数。
  3. 从视图控制器的viewWillTransition()调用该函数以处理设备旋转。
  4. 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)
    }
    

答案 4 :(得分:0)

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

答案 5 :(得分:0)

这应该可以使用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)
      }
    }
  }
}

答案 6 :(得分:-2)

你的约束只是一点点。看看这个,如果您有任何疑问,请告诉我。出于某种原因,我很难让视图的背景保持红色?所以我创建了一个填充视图,填补了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;
}

答案 7 :(得分:-2)

我将加上2美分,因为这个问题在Google中被高度编入索引。我认为你应该使用

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

ViewDidLoad中。另外,要将自定义UIView加载到Header,您应该使用viewForHeaderInSection委托方法。您可以为标头Nib笔尖设置自定义UIView文件。 Nib必须有一个控制器类,其子类{ - 1}}类似于 -

UITableViewHeaderFooterView

确保您的Nib文件名与类名相同,这样您就不会感到困惑,而且更容易管理。例如class YourCustomHeader: UITableViewHeaderFooterView { //@IBOutlets, delegation and other methods as per your needs } YourCustomHeader.xib(包含YourCustomHeader.swift)。然后,只需使用界面构建器中的身份检查器将class YourCustomHeader分配给Nib文件。

然后在主视图控制器的YourCustomHeader中注册Nib文件作为标题视图 -

viewDidLoad

然后在tableView.register(UINib(nibName: "YourCustomHeader", bundle: nil), forHeaderFooterViewReuseIdentifier: "YourCustomHeader") 中返回heightForHeaderInSection。这就是代表们的样子 -

UITableViewAutomaticDimension

这是一个更简单,更合适的方式,没有在接受的答案中建议的“Hackish”方式,因为多个强制布局可能会影响您的应用程序的性能,尤其是如果您的tableview中有多个自定义标头。按照我的建议执行上述方法后,您会注意到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 } (和Header)视图会根据自定义视图的内容大小进行神奇地展开和缩小(前提是您在自定义视图中使用AutoLayout ,即Footer,nib文件)。