UITableViewCell の UIView-Encapsulated-Layout-Height について


この記事について

デバッグによって得られたテーブルビューセルの高さの挙動についてのメモです。

環境

Xcode Version 11.5 (11E608c)

Self-sizing Table View Cells

UITableView は次のような設定によって、オートレイアウトベースでセルの高さを決定できるようになります。

tableView.estimatedRowHeight = 85.0
tableView.rowHeight = UITableView.automaticDimension

要点は2点です。

  • estimatedRowHeight0 以外の適切な値を設定する
  • rowHeightUITableView.automaticDimension を設定する

この設定によって、オートレイアウトベースでセルの高さを決定することができます。あとは、セルのコンテンツビュー内のレイアウト制約を過不足なく配置するだけです。

このように2つのラベルを配置して、親ビュー(コンテンツビュー)と兄弟ビューとの余白を設定すれば、次のような具合でレイアウトされると思います。

UIView-Encapsulated-Layout-Height

デバッグでレイアウト制約を確認してみると、身に覚えのないレイアウト制約が設定されていると思います。

実はこの制約がセルのコンテンツビューの高さを決定しているみたいです。

オートレイアウトベースでセルの高さを決定する場合は、このレイアウト制約は次のメソッドに依存しているみたいでした。

func systemLayoutSizeFitting(
  _ targetSize: CGSize, 
  withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, 
  verticalFittingPriority: UILayoutPriority
) -> CGSize

systemLayoutSizeFitting は、対象サイズとそのレイアウト優先度をパラメータとして指定することで、制約ベースにフィットするサイズを算出メソッドです。

試しに本来の処理を無視して、高さを固定してみます。

高さを100に固定したサンプル
import UIKit

class CustomCell: UITableViewCell {
  @IBOutlet weak var label1: UILabel!
  @IBOutlet weak var label2: UILabel!

  override func systemLayoutSizeFitting(
    _ targetSize: CGSize,
    withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority,
    verticalFittingPriority: UILayoutPriority
  ) -> CGSize {
    var size = super.systemLayoutSizeFitting(
        targetSize,
        withHorizontalFittingPriority: horizontalFittingPriority,
        verticalFittingPriority: verticalFittingPriority
    )
    size.height = 100
    return size
  }
}

オートレイアウトベースで決定するはずのセルの高さが固定になりました。

つまり、オートレイアウトベースで高さが決まるということは、『systemLayoutSizeFitting でセルのサイズを決める』ということだったんですね。

Self-sizing で実装するときの注意点

systemLayoutSizeFitting をオーバーライドしてブレークポイントを貼るなどして、どのようなパラメータが渡されるのかを確認することができます。

iPhone SE (2nd generation) の場合

targetSize horizontalFittingPriority verticalFittingPriority
(375.0, 0.0) 1000 50

対象サイズの高さを 0、垂直方向のレイアウト優先度を 50 にしていました。垂直方向の優先度を低く設定することで、コンテンツビュー内のレイアウト制約を優先させているようです。

こうすることで、対象サイズの高さをブレイクさせてフィットする高さを計算させているみたいです。

以上のことから、コンテンツビュー内部に配置したビューのレイアウト制約の優先度を 50 未満にしてしまうと意図しない高さになるかもしれないこともわかりました。

たとえば次のように、ラベルとコンテンツの下部の制約の優先度を 49 に変更してみます。

この場合、verticalFittingPriority の 50 が優先されるので意図しないレイアウトになりました。

通常は、50 未満の優先度を指定することはないと思いますが、優先度の設定には十分に気をつけたほうが良さそうですね。

これは iOS SDK の挙動なので、これに従うのがベターだと思いますが、もしも verticalFittingPriority50 が都合悪い場合は、これをオーバーライドすることで意図したセルの高さにすることができました。

垂直方向の優先度をもっと小さく指定する
override func systemLayoutSizeFitting(
    _ targetSize: CGSize,
    withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority,
    verticalFittingPriority: UILayoutPriority
) -> CGSize {
    return super.systemLayoutSizeFitting(
        targetSize,
        withHorizontalFittingPriority: horizontalFittingPriority,
        verticalFittingPriority: UILayoutPriority(1)
    )
}

こうすれば、インターフェースビルダーなどで配置したビューのレイアウト制約の優先度を 50 未満にしても意味のある値になると思います。

まとめ

  • オートレイアウトベースで高さを決めるということは、systemLayoutSizeFitting でセルの高さを決めるということだった
  • UIView-Encapsulated-Layout-Height は上記が算出するサイズの高さに一致する
  • この計算では、verticalFittingPriority50 が設定される
  • この点に気をつければ、テーブルビューセルの意図しない制約のブレイクを避けることができる