検証環境
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でレイアウトした後、実行すると以下のようなエラーが出ることがある。
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のドキュメントなどは見つからず、以下のサイトに言及があった。
どうやら、"UIView-Encapsulated-Layout-Height"はシステムが作成するCell(ContentView)の高さ制約のようである。
サンプルプロジェクトで作成した、Cell上の赤いビューは100pt
の高さであり、上下のマージンは50pt
ずつ制約を設定しているため、ContentViewの高さは200pt
の高さになるはずである。しかしながら、エラーのログからシステム側で作成した制約がUITableViewCellContentView:0x104c1daf0.height == 200.333
とContentViewに200.333pt
の高さを設定しようとしていることがわかる。
ビューデバッガで見てみる。スクショはiPhone8で実行したので値は200.5pt
と表示されている。
この制約のプライオリティは1000
に設定されている。となると、縦のAutoLayout制約を全て1000(UILayoutPriority.required)
で組んでいると矛盾が発生する。同じ200pt
の制約ならば、複数の制約をかけようとも矛盾が発生しないのでエラーとならないが200.333pt
と微妙に違う数値となっているため。矛盾を解消するために、システムが任意で選択した赤いビューの100pt
高さ制約を解除して回復を試みる、旨のメッセージが表示されている。
Will attempt to recover by breaking constraint <NSLayoutConstraint:0x281208910 UIView:0x104c07ee0.height == 100 (active)>
この0.333pt
分について、ふとセパレータの高さなのではないか? と推測した。
試しに、UITableView
のseparatorStyle
をnone
に変更したところ、エラーが発生しなくなった。ビンゴっぽい。
ビューデバッガで確認する。なるほどちゃんと"UIView-Encapsulated-Layout-Height"が200ptになっている。
- →システムは
UIView-Encapsulated-Layout-Height
制約をかけることで、CellのContentViewの高さを、セパレータ分を追加した高さに変更していると思われる。
システムが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)
等に変更する
-
(回避法1 - 2) 中身のビューやコンテナとなるビューに設定するマージン制約を
750(defaultHigh)
等に変更する
-
(回避法1 - 3) intrinsicContentSizeの制約のあるビューを用いることでも回避できる。UILabelや、カスタムクラスでintrinsicContentSizeをoverrideすることで高さを与えた場合なども制約は1000未満になるため有効。
- (回避法2) 現時点のiOSバージョンでは、
UITableView
のseparatorStyle
をnone
に変更しセパレータを表示しない場合は、付加分の高さがないため、制約がrequired(1000)であってもUIView-Encapsulated-Layout-Height
制約との競合が発生しないと思われる。
サンプルプロジェクト
これらを確認するために作ったサンプルプロジェクトは以下です。
Xcode 13.3 / iOS 15.4 / Swift version 5.6 で作成