UIStackViewの5つのdistributionを理解する

iOS 9から導入された`UIStackView`は、複雑になりがちなレイアウトを簡単に組むことができます。
導入から3年近くたった今、すでに使ったことのあるエンジニアさんも多くいらっしゃるのでないでしょうか。

しかし、使ってみると案外うまく使えなかったり、コンフリクトに遭遇してしまうことがありました。
そこで、`UIStackView`の中でも最も重要なプロパティの`distribution`について調べた結果をまとめました。

## この記事の内容

この記事では、5種類のある`UIStackViewDistribution`の全てでレイアウト例を示し、その**レイアウトがどのような制約によって成り立っているか解説します**。

注意して書いたつもりですが、もし内容に間違いや不備があればご指摘よろしくおねがいします :bow:

## .fill

まずは、最も基本の`.fill`でのレイアウト例を確認してみましょう。
各プロパティは以下のように設定しています。
axis: **.horizontal**
distribution: **.fill**
alignment: **.center**
spacing: **0**
![fill1-2.png](https://qiita-image-store.s3.amazonaws.com/0/130238/2b26f345-90f6-e719-720b-e42a7e39aa83.png)
図1-1

図1-1のレイアウトはどう構成されているのでしょうか。横方向の制約に番号をつけてみます。

![fill2-3.png](https://qiita-image-store.s3.amazonaws.com/0/130238/ebdc7a3a-398e-4fa1-ea50-af2f66c56cc5.png)
図1-2

図1-2の中の`制約1 ~ 4`はそれぞれ`UIStackView`の働きにより与えられた制約です。どんな制約かを列挙します。

1: stackView.left = imageView.left
2: imageView.right = label.left
3. label.right = rect.left
4. rect.right = stackView.right
 
となっています。1~4をひとつづつみていくと、`stackView`の左端から右端まで`arrangedSubviews`が連結するような制約なっています。その結果、`axis`の方向(この例では横方向)に`arrangedSubviews`がいっぱいに満たされています。
この**いっぱいに満たされる効果により`.fill`という名前がつけられています**。

#### 横幅の制約について
先ほどの`制約1 ~ 4`は、`arrangedSubviews`の両端をそろえる制約でした。では、横幅はどうやって決まっているのでしょうか。
今度は横幅の制約に番号を振ってみましょう。下図を見てください。

![fill3-5.png](https://qiita-image-store.s3.amazonaws.com/0/130238/7a5bb0de-0343-5f8a-9774-1e434902b8c9.png)
図1-3

制約の内容は以下です。

5: stackView.width = **your_constraint**
6: imageView.width = imageView.**intrinsicContentSize**.width
7: label.width = label.**intrinsicContentSize**.width

`制約5`は`stackView`の外部から与えられた制約です。プログラマーが明示的に与えた制約だったり、`stackView`が別の`UIStackView`の中にネストさせたりといった方法で与えられます。`制約5`がないとレイアウトを決定するのに必要な制約が足りず、レイアウトが崩れてしまう可能性があります。

`制約6`と`制約7`は`intrinsicContentSize`によるものです。`UIImageView`や`UILabel`はその内容によって`intrinsicContentSize`が与えられます。`UIImageView`だと写真の大きさ、`UILabel`だとテキストやフォントサイズによって決まります。
`rect`はただの`UIView`なので`intrinsicContentSize`はありません。ただ、`制約1 ~ 7`によって横幅が決まります(詳しくは図1-3を見てください)。

### `rect`に横幅の制約があった場合
最後に、エンジニアが`rect`に対して明示的に横幅の制約を与えた場合について考えてみましょう。
この場合`制約5`は不要になります。
`stackView`の横幅は
`stackView.width = imageView.width + label.width + rect.width`
と決定することができるからです。

## .fillEqually

`.fill`では、`UIStackView`は`arrangedSubview`に対して`axis`方向の大きさ(つまり横幅や高さ)の制約は与えませんでした。
しかし、`.fillEqually`にすると`UIStackView`は`arrangedSubview`に対して横幅や高さの制約を与えてくれます。
以下の条件でのレイアウト例をみてみましょう。

axis: **.horizontal**
distribution: **.fillEqually**
alignment: **.center**
spacing: **0**
![fillEqually1-2.png](https://qiita-image-store.s3.amazonaws.com/0/130238/2ebf83d9-c0d9-39f6-d1bd-018d3a12ec01.png)
図2-1

まず、`.fill`の時と同じく`arrangedSubview`は左端から右端までいっぱいに満たすようになります(図1-2と同じなので図は割愛します)。

横幅の制約は先ほどと大きく異なります。番号を振ってみましょう。

![fillEqually2-2.png](https://qiita-image-store.s3.amazonaws.com/0/130238/a76f3383-3c76-775d-d8d9-b2836b65ef6e.png)
図2-2

1: stackView.width = your_constraint
2, 3, 4: rect1.width = rect2.width = rect3.width

`制約1`は、`stackView`の外部からの制約です。`図1-3の制約5`と同じです。
`制約2`, `制約3`, `制約4`は`UIStackView`の働きによるもので、`rect1, rect2, rect3`の*横幅が全て同じ*という制約です。`stackView`の横幅(つまり`制約1`による横幅)を均等に3等分してくれます。
`axis`方向の大きさが全て同じという効果により**equally**という名前がつけられています。

## .fillProportionally
`.fillProportionally`は、`arrangedSubviews`に対する`axis`方向の制約の計算方法が`.fillEqually`と異なります。それ以外は同じです。

さっそく、以下の条件でのレイアウト例を見てみます。

axis: **.horizontal**
distribution: **.fillProportionally**
alignment: **.fill**
spacing: **0**

![fillProportionally1.png](https://qiita-image-store.s3.amazonaws.com/0/130238/78e6325a-6b90-1a93-0a1e-5f3267d35c95.png)
図3-1

名前に*fill*とある通り、`arrangedSubviews`は左端から右端まで満たされるようにいっぱいにレイアウトされます。これは`.fill`や`.fillEqually`と同じです。

横幅の制約に番号を振りましょう。`.fillEqually`との違いに注目してください。

![fillProportionally2-2.png](https://qiita-image-store.s3.amazonaws.com/0/130238/d9184d24-74a9-05a1-807a-9b4a4ce085ae.png)
図3-2

1: stackView.width = your_constraint
2, 3: bigLabel.width = smallLabel.width **✕ (5 / 3)**
2, 4: bigLabel.width = icon.width **✕ (5 / 2)**

`制約1`はこれまでと同様です。`stackView`の横幅は外部から与えられています。
`制約2 ~ 4`では`.fillEqually`と同様に`arrangedSubviews`の横幅同士がイコールで結ばれています。ただ、違うのは、**比率が設定されている**ことです。
この比率はどこから来ているかというと、`intrinsicContentSize`の比率です。この例では、実は

* bigLabel.intrinsicContentSize.width = 500pt
* smallLabel.intrinsicContentSize.width = 300pt
* icon.intrinsicContentSize.width = 200pt

という`intrinsicContentSize`になっています。したがって、比率にすると

* bigLabel.intrinsicContentSize.width : smallLabel.intrinsicContentSize.width = 5 : 3
* bigLabel.intrinsicContentSize.width : icon.intrinsicContentSize.width = 5 : 2

になります。それに合わせて`UIStackView`が横幅を設定してくれたのが`制約2 ~ 4`です。
`fillProportionally`というのは「比率を保ちながら満たす」という感じの意味からつけられた名前です。

#### 横幅の制約の優先度について
以上から、`bigLabel`の横幅には
1. `intrinsicContentSize`による横幅
2. `制約2`による横幅 

の二つの制約があるのですが、`制約2`の`priority`は1000(必須)です。一方、デフォルトでは`intrinsicContentSize`による制約の優先度は1000よりも低くなっています。そのため、`制約2`による制約が優先されます(他の`smallLabel`と`icon`も同様です)。
 
## .equalSpacing

`.equalSpacing`では`arrangedSubviews`の`axis`方向の大きさ(つまり横幅あるいは高さ)の決定はエンジニアの仕事です。`intrinsicContentSize`を使ったり、明示的に制約を与えましょう。また、UIStackViewの`axis`方向の大きさも外部から与えるのが基本です。

以下の設定でのレイアウト例を示します。
axis: **.horizontal**
distribution: **.equalSpacing**
alignment: **.fill**
spacing: **0**

![equalSpacing1-2.png](https://qiita-image-store.s3.amazonaws.com/0/130238/9dfe14ac-0422-2776-9437-82b452de383c.png)
図4-1

`.equalSpacing`の特徴は`arrangedSubviews`同士の間隔を`UIStackView`が動的に決定してくれることです。そのため`spacing`は通常は0に設定します。`spacing`の設定が0にもかからわず、ラベルやアイコンの間に隙間があることに注意してください。

隙間はどのような制約によって成り立っているか確認します。

![equalSpacing2-3.png](https://qiita-image-store.s3.amazonaws.com/0/130238/5e3b6a66-4758-645b-cd49-2644045e11d1.png)
図4-2

1: stackView.left = label1.left
2, 3: label2.left - label1.right = icon.left - label2.right 
4: icon.right = stackView.right

注) `label1, label2, icon`の横幅は`intrinsicContentSize`によるものです。

`制約1 ~ 4`は全て`UIStackView`の働きによるものです。
`制約2, 制約3`が重要です。`arrangedSubview`の横幅の制約に合わせて**隙間を同じ量だけ確保**しています。隙間が同じ量だけあるため、`equalSpacing`という名前になっています。

## .equalCentering

`.equalCentering`でも`arrangedSubviews`と`stackView`の`axis`方向の大きさの制約はエンジニアの仕事です。
また、`arrangedSubviews`同士の隙間は`UIStackView`が動的に設定してくれます。そのため`.equalSpacing`と同様に`spacing`の値は0にしておきます。

以下の設定でレイアウトしてみましょう。
axis: **.horizontal**
distribution: **.equalSpacing**
alignment: **.fill**
spacing: **0**
![equalCentering1.png](https://qiita-image-store.s3.amazonaws.com/0/130238/7027bf3e-3745-b4f0-baab-dfd2aa1938a9.png)
図5-1

`spacing`が0になっているにもかかわらず隙間があるのは`equalSpacing`と同じです。
隙間の計算方法を確認するために、制約に番号を振ってみましょう。

![equalCentering2-2.png](https://qiita-image-store.s3.amazonaws.com/0/130238/8f97e141-11f8-e942-ea68-490356ad1d2f.png)
図5-2

1: stackView.left = label1.left
2, 3: label2.center.x - label1.center.x = icon.center.x - label2.center.x
4: icon.right = stackView.right

注) `label1, label2, icon`の横幅は`intrinsicContentSize`によるものです。

`制約1 ~ 4`は全て`UIStackView`の働きによるものです。
`制約2, 制約3`は、`arrangedSubviews`の中心を基準にしていることに注目してください。中心同士の距離が等しいという制約になっています。この性質のため、**equalCentering**という名前がついています。

## 参考にした記事

以下の記事を参考にしました。

UIStackView Guide
https://developer.apple.com/documentation/uikit/uistackview

Let's Build UIStackView
http://kean.github.io/post/lets-build-uistackview

StackViewを賢く使ってらくちんAutoLayout
https://qiita.com/yucovin/items/ff58fcbd60ca81de77cb

Exploring UIStackView Distribution Types
https://spin.atomicobject.com/2016/06/22/uistackview-distribution/