web-dev-qa-db-ja.com

tableHeaderViewでのautolayoutの使用

複数行のUIViewを含むUILabelサブクラスがあります。このビューは自動レイアウトを使用します。

enter image description here

このビューをtableHeaderViewUITableViewとして設定したいと思います(notセクションヘッダー)。このヘッダーの高さは、ラベルのテキストに依存します。これは、デバイスの幅に依存します。このようなシナリオの自動レイアウトは優れているはずです。

私は manymanysolutions を見つけて試してみましたが、うまくいきませんでした。私が試したことのいくつか:

  • preferredMaxLayoutWidthの実行中に各ラベルにlayoutSubviewsを設定する
  • intrinsicContentSizeの定義
  • ビューに必要なサイズを把握し、tableHeaderViewのフレームを手動で設定します。
  • ヘッダーが設定されているときにビューに幅制約を追加する
  • 他のものの束

私が遭遇したさまざまな失敗のいくつか:

  • ラベルはビューの幅を超えて拡張され、折り返されません
  • フレームの高さは0です
  • アプリが例外Auto Layout still required after executing -layoutSubviewsでクラッシュする

ソリューション(または必要に応じてソリューション)はiOS 7とiOS 8の両方で機能するはずです。これはすべてプログラムで行われていることに注意してください。問題を確認するためにハッキングしたい場合のために、 小さなサンプルプロジェクト を設定しました。私は次の出発点に努力をリセットしました:

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;

何が欠けていますか?

17
Ben Packard

これまでの私自身の最良の答えは、tableHeaderViewを1回設定し、レイアウトパスを強制することです。これにより、必要なサイズを測定できます。これを使用して、ヘッダーのフレームを設定します。そして、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月の更新

残念ながら、これはまだ必要なようです。これは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)
22
Ben Packard

まだ解決策を探している人にとって、これは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を気にしません。 currenttableHeaderView 'ssizeを使用して、最初のセル/セクションヘッダーの場所を決定します。

1)幅制約を追加して、layoutIfNeeded()を呼び出すたびにtableHeaderViewがこの幅を使用するようにします。また、centerXおよびtop制約を追加して、tableViewを基準にして正しく配置します。

2)tableViewtableHeaderViewの最新のサイズを知らせるには、たとえば、デバイスが回転したときに、viewDidLayoutSubviewsでtableHeaderViewのlayoutIfNeeded()を呼び出します。次に、サイズが変更された場合は、beginUpdates/endUpdatesを呼び出します。

beginUpdates/endUpdatesを1つの関数に含めないことに注意してください。後で呼び出しを延期したい場合があるためです。

サンプルプロジェクトをチェックアウト

9
aunnnn

次の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. マジック!)headerViewに偽のlayoutViewを挿入します。現時点では、すべてのheaderView制約を削除し、layoutViewをheaderViewに展開して、最初のheaderView制約を復元する必要があります。 制約の順序に意味があるのは偶然です!正しい方法でheaderViewの高さを自動計算し、また正しい
    すべてのheaderViewサブビューのX集中。

  3. 次に、正しいtableViewを取得するために、headerViewを再レイアウトするだけです。
    交差せずにセクションの上に高さ計算とheaderViewを配置。

P.S。iOS8でも動作します。通常、ここではコード文字列をコメント化することはできません。

3
malex

ここでの回答のいくつかは、私が必要としているものに非常に近づくのに役立ちました。しかし、デバイスを縦向きと横向きの間で前後に回転させると、システムによって設定された「UIView-Encapsulated-Layout-Width」という制約との競合が発生しました。以下の私の解決策は、主にmarcoarment(彼への信用)によるこのGistに基づいています: https://Gist.github.com/marcoarment/1105553afba6b4900c1 。ソリューションは、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)
}
2
rene

これは、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)
      }
    }
  }
}
0
Everton Cunha

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))
    }
}
0
Carmelo Gallo