iOS 9から導入されたUIStackView
は、複雑になりがちなレイアウトを簡単に組むことができます。
導入から3年近くたった今、すでに使ったことのあるエンジニアさんも多くいらっしゃるのでないでしょうか。
しかし、使ってみると案外うまく使えなかったり、コンフリクトに遭遇してしまうことがありました。
そこで、UIStackView
の中でも最も重要なプロパティのdistribution
について調べた結果をまとめました。
この記事の内容
この記事では、5種類のあるUIStackViewDistribution
の全てでレイアウト例を示し、そのレイアウトがどのような制約によって成り立っているか解説します。
注意して書いたつもりですが、もし内容に間違いや不備があればご指摘よろしくおねがいします
.fill
まずは、最も基本の.fill
でのレイアウト例を確認してみましょう。
各プロパティは以下のように設定しています。
axis: .horizontal
distribution: .fill
alignment: .center
spacing: 0
図1-1
図1-1のレイアウトはどう構成されているのでしょうか。横方向の制約に番号をつけてみます。
図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
の両端をそろえる制約でした。では、横幅はどうやって決まっているのでしょうか。
今度は横幅の制約に番号を振ってみましょう。下図を見てください。
制約の内容は以下です。
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
図2-1
まず、.fill
の時と同じくarrangedSubview
は左端から右端までいっぱいに満たすようになります(図1-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
名前にfillとある通り、arrangedSubviews
は左端から右端まで満たされるようにいっぱいにレイアウトされます。これは.fill
や.fillEqually
と同じです。
横幅の制約に番号を振りましょう。.fillEqually
との違いに注目してください。
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
の横幅には
-
intrinsicContentSize
による横幅 -
制約2
による横幅
の二つの制約があるのですが、制約2
のpriority
は1000(必須)です。一方、デフォルトではintrinsicContentSize
による制約の優先度は1000よりも低くなっています。そのため、制約2
による制約が優先されます(他のsmallLabel
とicon
も同様です)。
.equalSpacing
.equalSpacing
ではarrangedSubviews
のaxis
方向の大きさ(つまり横幅あるいは高さ)の決定はエンジニアの仕事です。intrinsicContentSize
を使ったり、明示的に制約を与えましょう。また、UIStackViewのaxis
方向の大きさも外部から与えるのが基本です。
以下の設定でのレイアウト例を示します。
axis: .horizontal
distribution: .equalSpacing
alignment: .fill
spacing: 0
.equalSpacing
の特徴はarrangedSubviews
同士の間隔をUIStackView
が動的に決定してくれることです。そのためspacing
は通常は0に設定します。spacing
の設定が0にもかからわず、ラベルやアイコンの間に隙間があることに注意してください。
隙間はどのような制約によって成り立っているか確認します。
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
図5-1
spacing
が0になっているにもかかわらず隙間があるのはequalSpacing
と同じです。
隙間の計算方法を確認するために、制約に番号を振ってみましょう。
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/