【问题标题】:Using autolayout in a tableHeaderView在 tableHeaderView 中使用自动布局
【发布时间】:2015-03-20 16:52:02
【问题描述】:

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

我想将此视图设置为UITableViewtableHeaderView不是节标题)。此标题的高度将取决于标签的文本,而标签的文本又取决于设备的宽度。这种场景自动布局应该很擅长。

我已经找到并尝试了 many many solutions 以使其正常工作,但无济于事。我尝试过的一些事情:

  • 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;

我错过了什么?

【问题讨论】:

  • 我对你现在拥有的东西有什么问题感到困惑?截图看起来不错……
  • 对于截图,我手动在标题上设置了一个框架。
  • @BenPackard 我也为此苦苦挣扎了一段时间,据我所知,不可能让tableHeaderView 尊重自动布局。最终,我在包含所有内容的标题视图中添加了一个额外的视图。在表格视图的超级视图的layoutSubviews 中,我抓取了包装器的大小并手动设置了tableHeaderView 的框架。
  • @AnthonyMattox 这似乎比我自己的解决方案差。我无法让它工作。你能提供更多信息吗?具体来说,你在layoutSubviews 中做什么来计算必要的大小?
  • @BenPackard 我可能有点不对劲。在我遇到的情况下,表格标题视图中正在进行更多自定义布局,因此它可能是偶然的。我想出一个干净的工作示例,但tableHeaderView 非常挑剔。

标签: ios uitableview uiview autolayout nslayoutconstraint


【解决方案1】:

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

【讨论】:

    【解决方案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)。 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
    }
    

    这是一种更简单且适当的方式,无需在已接受的答案中建议的“Hackish”方式,因为多个强制布局可能会影响您的应用程序的性能,特别是如果您的表格视图中有多个自定义标题。按照我的建议执行上述方法后,您会注意到您的 Header(和或 Footer)视图会根据您的自定义视图的内容大小神奇地扩展和收缩(前提是您在自定义视图中使用 AutoLayout,即 @987654343 @,笔尖文件)。

    【讨论】:

    • 这是一个很好的解释如何实现节标题,但问题是关于tableHeaderView
    【解决方案3】:

    这里的一些答案帮助我非常接近我所需要的。但是在纵向和横向之间来回旋转设备时,我遇到了与系统设置的约束“UIView-Encapsulated-Layout-Width”的冲突。我下面的解决方案主要基于 marcoarment 的这个要点(归功于他):https://gist.github.com/marcoarment/1105553afba6b4900c10。该解决方案不依赖于包含 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)
    }
    

    【讨论】:

      【解决方案4】:

      对于仍在寻找解决方案的任何人,这适用于 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 tableHeaderViewsize 来确定第一个单元格/节标题应该在哪里。

      1) 添加一个宽度约束,以便tableHeaderView 在我们调用 layoutIfNeeded() 时使用此宽度。还要添加 centerX 和 top 约束,使其相对于tableView 正确定位。

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

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

      Check out a sample project

      【讨论】:

      • 一切正常。虽然我需要添加高度约束 headerView.heightAnchor.constraint(equalToConstant: 412).isActive = true
      • 您的 widthAnchor 可能是多余的,来自docs:“表格视图仅考虑您的视图框架矩形的高度;它会自动调整页脚视图的宽度以匹配表格视图的宽度。 "
      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        以下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
        

        解释“奇怪”的步骤:

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

        2. 那么我们只需要再次重新布局headerView就可以得到正确的tableView
          高度计算和headerView定位在上面没有的部分 相交。

        附注它也适用于 iOS8。在一般情况下,这里不可能注释掉任何代码字符串。

        【讨论】:

        • 这唯一真正有效的解决方案......真是令人抓狂。谢谢!
        【解决方案7】:

        到目前为止,我自己的最佳答案是设置一次 tableHeaderView 并强制布局通过。这允许测量所需的大小,然后我用它来设置标题的框架。而且,与tableHeaderViews 一样,我必须再次设置它第二次以应用更改。

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

        【讨论】:

        • 这一定是iOS的一个bug?!无论如何,谢谢你,我会去实施它。我一直在用你在问题中链接到的许多“解决方案”来扯掉我的头发。
        • 太烦人了!我不得不手动将框架设置为所需高度的两倍,以使 tableFooterView 不为零高度
        • 如果我将代码添加到viewDidAppear,我只能让它工作,但在它转换到新大小之前会快速闪烁。如果我将代码添加到viewDidLoad 它没有效果。奇怪的是,如果我将它添加到viewWillLayoutSubviews,除非我旋转显示器然后再旋转回来,否则它没有任何效果。
        • 尝试制作以下 headerView:x 中心方形 UIView 在距顶部 10 pt 处,x 中心 UILabel 在 UIView 下,在距底部 10 pt 处相隔 10 pt。 (例如,FB Messenger 设置屏幕的标题)。此框架解决方案不适用于所描述的情况。
        • @Ben Packard,似乎这种解决方法不再适用于 iOS 10。最后一行`self.tableHeaderView = header`用正确的帧设置标题。尽管如此,viewDidLayoutSubviews 框架内还是不正确的 (frame = (0 0; 375 0);)
        【解决方案8】:

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

        【讨论】:

        • 不确定您是否误解了我的问题,但我不是在寻求布置消息视图的帮助。此外,您需要填充视图的原因是消息视图本身的高度为 0。标签仅可见,因为它们溢出边界。设置 clipsToBounds = YES 你就会明白我的意思了。如果您将此视图安装为 tableHeaderView,则表格的单元格将隐藏标签,因为标题的高度为 0。
        猜你喜欢
        • 2013-08-19
        • 2015-03-10
        • 1970-01-01
        • 2014-01-25
        • 2015-06-23
        • 2015-07-06
        • 2015-12-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多