UITableViewCell での UIView-Encapsulated-Layout-Height 制約との競合エラーに対処する

## 検証環境
Xcode 13.3　/ iOS 15.4 / Swift version 5.6 

## 要点

#### 問題
* UITableViewCellの中身を、縦のAutoLayout制約を全て`1000(UILayoutPriority.required)`で組むと、システムが作成する`UIView-Encapsulated-Layout-Height`制約との競合が発生する。

#### 解決法
→ 縦のAutoLayoutどこかの制約を`1000`未満（`999`以下）のプライオリティに変更する。

#### 理由
実行時にシステムはCellのContentViewの高さをセパレータ分を含めた高さを確定し、変更していると思われ、縦の制約のすべてがRequiredの場合、その制約と競合してしまう。

## 概要

### 'UIView-Encapsulated-Layout-Height'とは？

UITableViewCellをAutoLayoutでレイアウトした後、実行すると以下のようなエラーが出ることがある。

```Swift
2022-04-05 23:26:25.295287+0900 CellLayoutSolution[27039:2060062] [LayoutConstraints] Unable to simultaneously satisfy constraints.
	Probably at least one of the constraints in the following list is one you don't want. 
	Try this: 
		(1) look at each constraint and try to figure out which you don't expect; 
		(2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x281208910 UIView:0x104c07ee0.height == 100   (active)>",
    "<NSLayoutConstraint:0x281208960 V:|-(50)-[UIView:0x104c07ee0]   (active, names: '|':UITableViewCellContentView:0x104c1daf0 )>",
    "<NSLayoutConstraint:0x2812089b0 V:[UIView:0x104c07ee0]-(50)-|   (active, names: '|':UITableViewCellContentView:0x104c1daf0 )>",
    "<NSLayoutConstraint:0x281208e60 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x104c1daf0.height == 200.333   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x281208910 UIView:0x104c07ee0.height == 100   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

```

特に `"<NSLayoutConstraint:0x281208e60 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x104c1daf0.height == 200.333   (active)>"`

の部分について、身に覚えのない制約が追加されていることがわかる。

これについて検索すると、Appleのドキュメントなどは見つからず、以下のサイトに言及があった。

[StackOverflow - What is NSLayoutConstraint "UIView-Encapsulated-Layout-Height" and how should I go about forcing it to recalculate cleanly?](https://stackoverflow.com/a/25795758)

どうやら、<b>"UIView-Encapsulated-Layout-Height"はシステムが作成するCell（ContentView）の高さ制約</b>のようである。

サンプルプロジェクトで作成した、Cell上の赤いビューは`100pt`の高さであり、上下のマージンは`50pt`ずつ制約を設定しているため、ContentViewの高さは`200pt`の高さになるはずである。しかしながら、エラーのログからシステム側で作成した制約が`UITableViewCellContentView:0x104c1daf0.height == 200.333`とContentViewに`200.333pt`の高さを設定しようとしていることがわかる。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/b7e33fb5-fa4f-b03c-b6b6-66c33b2329ee.png">

ビューデバッガで見てみる。スクショはiPhone8で実行したので値は`200.5pt`と表示されている。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/6fb8526f-2f68-1e98-a075-5f3f57cdc896.png">

この制約のプライオリティは`1000`に設定されている。<b>となると、縦のAutoLayout制約を全て`1000(UILayoutPriority.required)`で組んでいると矛盾が発生する。</b>同じ`200pt`の制約ならば、複数の制約をかけようとも矛盾が発生しないのでエラーとならないが`200.333pt`と微妙に違う数値となっているため。矛盾を解消するために、システムが任意で選択した赤いビューの`100pt`高さ制約を解除して回復を試みる、旨のメッセージが表示されている。

`Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x281208910 UIView:0x104c07ee0.height == 100   (active)>`

この`0.333pt`分について、ふとセパレータの高さなのではないか？ と推測した。
<b>試しに、`UITableView`の`separatorStyle`を`none`に変更したところ、エラーが発生しなくなった。</b>ビンゴっぽい。

<img width="800" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/90ecebad-68fe-8979-ed13-9413e2d31ab6.png">

ビューデバッガで確認する。なるほどちゃんと"UIView-Encapsulated-Layout-Height"が200ptになっている。

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/9144ce3f-85eb-b994-be52-9115d996885c.png">

* <b>→システムは`UIView-Encapsulated-Layout-Height`制約をかけることで、CellのContentViewの高さを、セパレータ分を追加した高さに変更していると思われる。</b>

システムがSelf-Sizingなセルの高さを割り出すためには、縦方向の制約が設定されている必要があるのだけれども、`UIView-Encapsulated-Layout-Height`制約を使ってセパレータを含めた高さに変更する際に、全てRequiredの制約だと衝突するということである。

#### 'UIView-Encapsulated-Layout-Height'との競合を回避するには？

* （回避法1） セパレータを表示するCellでは、どのようにするかというと、縦のAutoLayoutどこかの制約を`1000`未満（`999`以下）のプライオリティに変更する。一箇所でも1000以下の縦の制約があると、エラーは発生しない。

システムは縦方向の制約からCellの高さを計算し、次にセパレータ分を含めてCellの高さを確定させる`UIView-Encapsulated-Layout-Height`制約（Required）を発行し適用する。高さ計算が終わった後999以下に落とした制約は最終的に無視されるので競合しない、というシナリオになると思う。

   * （回避法1 - 1） 中身のビューに設定する高さ制約を`750(defaultHigh)`等に変更する
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/9e39516d-1457-a91a-6541-6f08b570600f.png">


   * （回避法1 - 2） 中身のビューやコンテナとなるビューに設定するマージン制約を`750(defaultHigh)`等に変更する
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/198e8231-d0d7-ae05-e619-90460e19475b.png"><img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/c8cf5fe0-a52e-38ca-75f7-0af53399b428.png">


   * （回避法1 - 3） intrinsicContentSizeの制約のあるビューを用いることでも回避できる。UILabelや、カスタムクラスでintrinsicContentSizeをoverrideすることで高さを与えた場合なども制約は1000未満になるため有効。

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/32e0695a-e22e-a6a9-0689-a61c70211ce9.png">
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/bd713a20-c6f1-d793-f1be-c3a97f03339e.png"><img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53174/8abda640-af68-073d-1b2f-3c559e597e59.png">

* （回避法2） 現時点のiOSバージョンでは、`UITableView`の`separatorStyle`を`none`に変更しセパレータを表示しない場合は、付加分の高さがないため、制約がrequired(1000)であっても`UIView-Encapsulated-Layout-Height`制約との競合が発生しないと思われる。

## サンプルプロジェクト

これらを確認するために作ったサンプルプロジェクトは以下です。

* [https://github.com/snoozelag/CellLayoutSolution](https://github.com/snoozelag/CellLayoutSolution)

Xcode 13.3　/ iOS 15.4 / Swift version 5.6 で作成