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

## この記事について

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

##### 環境

Xcode Version 11.5 (11E608c)

## Self-sizing Table View Cells

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

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

要点は２点です。

- `estimatedRowHeight` に `0` 以外の適切な値を設定する
- `rowHeight` に `UITableView.automaticDimension` を設定する

この設定によって、オートレイアウトベースでセルの高さを決定することができます。あとは、セルのコンテンツビュー内のレイアウト制約を過不足なく配置するだけです。
![carbon (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/f3d4dc0a-d1e1-a4f8-c699-d8134180fd9f.png)
このように２つのラベルを配置して、親ビュー（コンテンツビュー）と兄弟ビューとの余白を設定すれば、次のような具合でレイアウトされると思います。

<img width="450" alt="Screen Shot 2020-05-30 at 16.48.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/f3adacb2-67e0-5351-6fc4-9d919263b54f.png">

## UIView-Encapsulated-Layout-Height

デバッグでレイアウト制約を確認してみると、身に覚えのないレイアウト制約が設定されていると思います。
<img width="896" alt="Screen_Shot_2020-05-30_at_16_53_38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/6487c806-1a3f-a84c-deb8-4f733c083910.png">
実はこの制約がセルのコンテンツビューの高さを決定しているみたいです。

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

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

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

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

```swift:高さを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
  }
}
```

オートレイアウトベースで決定するはずのセルの高さが固定になりました。
<img width="450" alt="Screen Shot 2020-05-30 at 17.01.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/0476f8c2-1574-3ec3-fd5e-00da169e582d.png">
つまり、オートレイアウトベースで高さが決まるということは、**『systemLayoutSizeFitting でセルのサイズを決める』**ということだったんですね。

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

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

**iPhone SE (2nd generation) の場合**

| targetSize | horizontalFittingPriority | verticalFittingPriority |
| ------- | ------- | ------- |
| (375.0, 0.0) | 1000 | 50 |

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

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

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

たとえば次のように、ラベルとコンテンツの下部の制約の優先度を 49 に変更してみます。
![carbon (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/d1fce968-8fd1-d346-5560-7482fbb48495.png)

この場合、`verticalFittingPriority` の 50 が優先されるので意図しないレイアウトになりました。
<img width="450" alt="Screen Shot 2020-05-30 at 17.04.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63842/d42a8c7f-b4d2-02e8-99a8-e51b15311217.png">
通常は、`50` 未満の優先度を指定することはないと思いますが、優先度の設定には十分に気をつけたほうが良さそうですね。

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

```swift:垂直方向の優先度をもっと小さく指定する
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** は上記が算出するサイズの高さに一致する
- この計算では、`verticalFittingPriority` に `50` が設定される
- この点に気をつければ、テーブルビューセルの意図しない制約のブレイクを避けることができる