[AutoLayout] Intrinsic Content Sizeを活用しよう〜その1 概要編〜

## 始めに

こんにちは、[Life is Tech ! #1 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/life-is-tech)の22日目を担当しますふみっちです。

今回はiOSアプリ開発における`Intrinsic Content Size`に関する知見が最近深まってきたのでそれに関して話していけたらいいなと思います。どうぞお付き合いください。

また、この記事は〜その1 概要編〜となっていて、`Intrinsic Content Size`に関する説明を行います。実装とそれに関する解説は[Life is Tech ! #2 Advent Calendar 2020　22日目](https://qiita.com/fummicc1_dev/private/b0078216c9d8fb9281a2)に書かせてもらったので合わせて確認してもらえると嬉しいです☺️

## 主題

AutoLayoutは制約を用いてUIView同士の相対的な距離を決定します。その制約は一度に有効となる個数に限りがあります。
いくつの制約を付与すればいいのかは作成したいレイアウトによって様々ですが、「このLabelはどのくらいの高さにしようか」といった**1ptレベルでの細かい数字の決め**であったり、「このLabelは最初は1行だけど、ボタンを押したら2行にしたいな」といった**動的なサイズの変化**に関しては表示するデータの内容を考慮する必要があり、実装が複雑になることもあると思います。
`Intrinsic Content Size`を活用することで、横幅や高さを具体的に指定する必要がなくなったり、動的なサイズの変化に対応することができます。
そこで今回は`Intrinsic Content Size`の概要を説明します！

## AutoLayoutに必要な制約の個数

一つのUIViewが有効にできる制約の個数はx軸で2つ、y軸で2つです。（ただ、例外として、手元で確認したところUIStackViewに関してはx軸・y軸ともに3つの制約が確認できました。）

また、同じ[Attribute](https://developer.apple.com/documentation/uikit/nslayoutconstraint/attribute)に関する制約はいくら付与しても有効になるのは一つです。なので、`width`の制約を2つ付与したとしてもx軸に関して2つの制約を付与したことにはならない点に注意が必要です。

例えば以下の組み合わせは全て軸に対して2つの制約を有効にすることができます。

|x軸|y軸|
|---|---|
|`width` `centerX`|`height` `centerY`|
|`width` `trailing`|`height` `top`|
|`leading` `centerX`|`bottom` `centerY`|

一方で、以下の制約は同じAttributeに対しての制約となるためどちらか一方しか有効にならず、x軸で2つの制約を追加したことになりません。

```swift
// item2の横幅の2倍の長さに30を足した大きさを横幅に設定
item1のwidthに関するConstraint = item2のwidthに関するAttribute * 2 + 30

// item2の横幅に90を足した大きさを横幅に設定
item1のwidthに関するConstraint = item2のwidthに関するAttribute * 1 + 90
```

## 必要以上に制約を付与した場合

次に、必要以上に制約を付与した場合に関して、実際の挙動と共に確認します。

UILabelにx軸に関して3つ、y軸に関して3つの制約を付与してみましょう。  

|UILabelに合計6個の制約を付与|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/too_many_constraints.png?raw=true)|

Xcode上ではエラーもなく正しく動いているように見えますが、実際には制約はそれぞれの軸で2つしか有効でないため、下の画像のように無視される制約（薄くなっている制約）が発生します。

|実際に有効な制約|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/too_many_constraints_result.png?raw=true)|

また、デバッグ前にXcode上で上記の問題を検知したい場合は、Storyboard上のiPhoneの種類を変更してみるのが手軽です。  
ここでは触れませんが、StorybaordのPreviewを機能を用いると複数のサイズを一度に確認することもできます。

|iPhone SE 2→iPhone 11に変更|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/too_many_constraints_change_device.png?raw=true)|

## 制約の個数がx軸に2つy軸に2つに満たない場合

今度は逆に制約の個数が4つに満たない場合を考えてみましょう。  
この場合は、`Intrinsic Content Size`についても考慮する必要があります。

## Intrinsic Content Sizeとは

`Intrinsic Content Size`とは**UIViewの理想的なサイズ**のことです。例えば、UILabelに関しては**テキストが過不足なく収まるサイズ**ということになります。

|通常のUILabelにおけるIntrinsic Content Size|複数行の場合は自動でリサイズされる|
|---|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/1x/intrinsic_content_size.png?raw=true)|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/1x/intrinsic_content_size_2.png?raw=true)|

### Intrinsic Content Sizeは制約となる

Sample1として例をみていきましょう  
画面中央にUILabelを一つ配置し、以下のように`centerX`と`centerY`の制約を付与します。

|centerXとcenterYのAttributeを持つ制約の追加|
|---|
|![add_constraints_new.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/f7e02138-f97f-d999-8f4b-82714c737ff8.gif)|

上の操作によって`centerX`と`centerY`の制約をUILabelに追加したため、Phone 12 Pro Maxで実行してもUILabelは画面の中央に配置されます。

|制約を追加後のUILabel|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/label_center_iphone12promax_fixed.png?raw=true)|

上で説明したように、制約はx軸で2つy軸で2つの合計4つ必要ですが、`centerX`と`centerY`の2つのみに制約を付与した場合でも正常に動作します。  
これは`Intrinsic Content Size`が制約として働くためです。

|Sample1のUILabelの制約|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/a_few_constraints_result.png?raw=true)|

上の画像からも、Storyboard上では二つしか制約を付与していないはずなのに、実際には制約は4つ有効になっていることが分かると思います。  

**ただし、Labelの文字が長くなって画面の枠からはみ出してしまう場合は、以下のように端からの最小距離を設定しておくと必要があります。**

|Intrinsic Content Sizeが画面よりも大きい場合|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/over_intrinsic_content_size_problem.png?raw=true)|

|Storybaord上で端からの最小距離を32ptに設定|実際の動作|
|---|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/over_intrinsic_content_size_storyboard.png?raw=true)|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/over_intrinsic_content_size.png?raw=true)|

また、**`centerX`や`centerY`ではなく、`width`または`height`に関しての制約を付与した場合**は`Intrinsic Content Size`による制約と`Attribute`が重複してしまうため、`Intrinsic Content Size`が無効になってしまう点に注意が必要です。

|Storyboard|Debug|
|---|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/invalid_intrinsic_content_size_storyboard.png?raw=true)|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample1/invalid_intrinsic_content_size_result.png?raw=true)|

---

## Content Hugging Priority

`Intrinsic Content Size`を持つ複数のUIViewに対して表示領域（画面サイズなど）が**大きい**場合に`Content Hugging Priority`を気にする必要があります。

`Content Hugging Priority`が**大きい**ほど、他のUIViewよりも`Intrinsic Content Size`が優先され理想的なサイズを維持することができます。
一方、`Content Hugging Priority`が**小さい**ほど、`Intrinsic Content Size`は無視されやすく、表示領域が大きいため理想的なサイズよりも大きくなります。

以下がそれぞれのUIViewが持つ`Content Hugging Priority`の値の一覧です。（一部）

|部品名|Content Hugging Priority|
|---|---|
|UILabel|251|
|UIImageView|251|
|UITextField|250|
|UIButton|250|
|UIPickerView|250|

同じ`Content Hugging Priority`を並べて制約を過不足なく追加すると、優先度（Priority）を判断できずエラーが発生します。

例えば、UILabelとUIImageViewは共に`Content Hugging Priority`が251に設定されているため以下のように制約エラーが発生してしまいます。

|理想的なサイズよりも画面領域が広いためUIViewを理想的なサイズで表示できずエラーが発生|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/hugging_priority_viewController.png?raw=true)|

|制約|エラー内容|
|---|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/hugging_priority_tree.png?raw=true)|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/hugging_error.png?raw=true)|

このようなエラーが発生した場合は関係するUIView同士が異なる`Content Hugging Priority`の値を持つように変更してあげます。
説明したように`Content Hugging Priority`が大きいほど`Intrinsic Content Size`は維持されます。今回は`UILabel`の`Content Hugging Priority`を1上げて252にしてあげます。

|変更後のUILabel|変更後のUIImageView|
|---|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/hugging_priority_error_fix_label.png?raw=true)|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/hugging_priority_error_fix_imageView.png?raw=true)|

## Content Compression Resistance Priority

`Intrinsic Content Size`を持つ複数のUIViewに対して表示領域（画面サイズなど）が**小さい**場合に`Content Compression Resistance Priority`を気にする必要があります。

`Content Compression Resistance Priority`が**大きい**ほど、他のUIViewよりも`Intrinsic Content Size`が優先され理想的なサイズを維持することができます。
一方、`Content Compression Resistance Priority`が**小さい**ほど、`Intrinsic Content Size`は無視されやすく、表示領域が小さいため理想的なサイズよりも小さくなります。

以下がそれぞれのUIViewが持つ`Content Compression Resistance Priority`の値の一覧です。（一部）

|部品名|Content Compression Resistance Priority|
|---|---|
|UILabel|750|
|UIImageView|750|
|UITextField|750|
|UIButton|750|
|UIPickerView|750|

基本的に全てのUIViewは一律で`Content Compression Resistance Priority`は750に設定されています。

今回は、UILabelとUIImageViewのグループを3つ縦に並べる場合を考えます。本来は全てのUIViewの`Intrinsic Content Size`を尊重したいですが、そうすると以下のように画面に収まりきらずエラーが発生してしまいます。

|画面に部品が収まりきらずエラーが発生|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/resistance_imageView_error.png?raw=true)|

|制約|エラー内容|
|---|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/resistance_tree.png?raw=true)|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/resistance_error.png?raw=true)|

`Content Compression Resistance Priority`が全て750となっているため、どのUIViewを縮小すれば良いかを判断できないためです。  
なので、一番下のUIImageViewの`Content Compression Resistance Priority`を750から749に変更して、制約に合わさるサイズで表示します。

|一番下のUIImageViewの`Content Compression Resistance Priority`を749に変更|
|---|
|![](https://github.com/fummicc1-lit/AdventCalendar2020/blob/main/Doc/Assets/Sample3/resistance_fix_imageView.png?raw=true)|

このように複数の`Intrinsic Content Size`を考慮する必要がある場合は`Content Hugging Priority`や`Content Compression Resistance Priority`を確認する必要があります。

---

ここまでを理解すれば`Intrinsic Content Size`はもう理解したと言っても過言ではないと思います！
これからは、実際に自分でレイアウトを組んでトライアンドエラーをして経験を積んでいきましょう！😎

|この記事を読んでIntrinsic Content Sizeを少しでも理解してもらえたら最高！|
|---|
|![undraw_order_confirmed_aaw7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/300444/880cb6ab-7b59-2560-221d-d83bdd9176c8.png)
|

---

ここまで読んでいただたきありがとうございます！最後に発展としていくつかセクションがあるので余力があれば合わせて読んでみてください😁

<details><summary>発展</summary><div>

## 発展

## UIView.noIntrinsicMetric

標準の段階でも全てのUIViewが理想的なサイズ（`Intrinsic Content Size`)を設定できているわけではありません。
例えば、ただのUIViewやUIScrollViewは中身が不明であったりスクロールできる故に中身のサイズ≠コンテナのサイズという様なことが考えられます。
そのようなUIViewの`Intrinsic Content Size`は`width`も`height`も共に`UIView.noIntrinsicMetric`という値に設定されています。

|`Intrinsic Content Size`が無効なViewの一例|
|---|
|UIView|
|UITableView|
|UICollectionView|
|UIStackView|
|UITextView（スクロール可能な場合）|

`UITableView`や`UICollectionView`やスクロール可能な場合の`UITextView`は`UIScrollView`を継承しているという共通点があります。

## Intrinsic Content Sizeをカスタマイズする

上記のような`Intrinsic Content Size`が無効なUIViewに対しても、`Intrinsic Content Size`は`UIView`のプロパティであるため、クラスを継承すれば自分でカスタマイズをして有効にすることもできます。
例えば、以下のように`UIView`を継承した`HalfModalView`に`Intrinsic Content Size`を設定することもできます。

```swift
class HalfModalView: UIView {
    override var intrinsicContentSize: CGSize {
        let height: CGFloat = UIScreen.main.bounds.size.height / 2
        let width: CGFloat = UIScreen.main.bounds.size.width
        return CGSize(width: width, height: height)
    }
}
```

上のHalfModalViewは`Intrinsic Content Size`が有効なため、追加する制約はx軸に一つy軸に一つで問題ありません。  

また、以下の`PaddingLabel`は通常の`UILabel`に比べて、中央揃えの場合には、右に`16pt`左に`16pt`、上に`8pt`下に`8pt`の余白をつけます。

```swift
class PaddingLabel: UILabel {
    override var intrinsicContentSize: CGSize {
        var size: CGSize = super.intrinsicContentSize
        size.width += 32
        size.height += 16
        return size
    }
}
```

</div>
</details>

---


## 最後に

自分は`Intrinsic Content Size`を知ってからAutoLayoutの制約の組み方が劇的に分かるようになったので、同じようにこの記事をみて少しでも`Intrinsic Content Size`に興味が生まれたり関心が高まるととても嬉しいです！🎉  
具体的な実装例として[[AutoLayout] Intrinsic Content Sizeを活用しよう〜その2 Self Sizing編〜](https://qiita.com/fummicc1_dev/private/b0078216c9d8fb9281a2)も合わせて読んでみてください〜
最後まで読んでくださりありがとうございました！