遅ればせながら UIStackView 入門

仕事で関わっているプロジェクトはiOS8のサポートを打ち切ったこともあり、2016年の師走に今更ながらUIStackViewについて調べてみました。

# UIStackViewとは
- AutoLayoutのラッパークラス
- iOS9以上で利用可能
- 煩雑な制約を使わずに複数のViewを水平/垂直方向にレイアウト可能
- デバイスの回転、スクリーンサイズなどに動的に対応可能

| Horizontal | Vertical |
|:---:|:---:|
|<img width="505" alt="axis-horizontal.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/879b22d0-2b61-08d3-23c5-f3e1cce7b0ab.png">|<img width="505" alt="axis-vertical.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/115a359a-d8d6-b1ee-8664-3c79c71a4a26.png">|

## レイアウトに関わるプロパティ
UIStackViewは主に次の4つのプロパティを使って設定します。

- `axis` `alignment` `distribution` `spacing`

### `axis`
`var axis: UILayoutConstraintAxis`

サブビューを X軸方向に並べる場合は `horizontal`、 Y軸方向に並べる場合は `vertical` を指定します。

### `alignment`
`var alignment: UIStackViewAlignment`

`axis` と垂直軸方向におけるサブビューの配置方法を指定します。

`axis` が `horizontal` の場合、`top` `center` `bottom` `fill` が指定可能です。
また、`vertical` の場合、 `leading` `center` `trailing` `fill` が指定可能です。
`fill` 以外に関しては、各サブビューは *Intrinsic Content Size* に基づいてサイズが決定されます。
`fill` では、axisとの垂直方向の領域を埋めるようにして各ビューがリサイズされます。

##### axis: .horizontal の場合
<img width="211" alt="alignment-horizontal.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/ad82502f-2edc-6136-6d16-f9cc289d0b30.png">

##### axis: .vertical の場合
<img width="291" alt="alignment-vertical.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/9bd9efd2-6f17-c878-2082-a86273aa022e.png">

### `distribution`
`var distribution: UIStackViewDistribution`

`axis` 方向におけるサブビューの配置方法を指定します。

`fill` `fillEqually` `FillProportionally` `EqualSpacing` `EqualCentering` が指定可能です。

- `fill`
  - `hugging priority` `compression resistance priority` を元にサブビューをリサイズします。曖昧な場合、 `arrangedSubviews` （後述） の順に基づいてサブビューをリサイズします。

- `fillEqually`
  - 各ビューを同一幅で配置します。
- `fillProportionally`
  - *Intrinsic Content Size* に応じて幅が決まる
  - 各サブビューは係数（StackViewの幅 / 各サブビューの幅の総和 ?）をかけてリサイズされる
- `equalSpacing`
  - *Intrinsic Content Size* に応じて幅が決まる
  - サブビュー間のスペースを等間隔に配置
- `equalCentering`
  - 各サブビューの中心を等間隔に配置
  - サブビューの幅の総和がStackViewより大きい場合、幅はリサイズされる。`

##### axis: .horizontal の場合
<img width="317" alt="distribution-horizontal.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/e96fc1e5-be48-8f18-4e4e-89aaad361c6a.png">

##### axis: .vertical の場合
<img width="617" alt="distribution-vertical.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/f3e6935f-e930-b7af-4cff-c4cc99cb7722.png">

### `spacing`
`var spacing: CGFloat`

サブビュー間の余白を指定します。 `distribution` が `equalSpacing` や `equalCentering` の場合、無視されます。


## サブビューの管理方法
UIStackViewはUIViewのサブクラスのため `subviews` プロパティを持ちますが、実際のレイアウトは `arrangedSubviews` プロパティのサブビューが対象となります。
`arrangedSubviews` への追加は `addArrangedSubview()` を利用します。`arrangedSubviews` は `subviews` のサブセットのため、このメソッド経由で追加されたビューは `subviews` に追加されます。
なお、 ビューを `addSubview()` で `subviews` に追加しただけでは `arrangedSubviews` には追加されず、UIStackViewでのレイアウト制御の対象として扱われないので注意が必要です。

# UIStackViewの作り方
UIStackViewを使って次のようなビュー構成を作る場合のサンプルです。
（※Storyboardの場合の例は割愛）

<img width="227" alt="make-programatically.png" src="https://qiita-image-store.s3.amazonaws.com/0/31069/7c14e1c4-f1fc-59e1-8c19-55e51e06b161.png">


```swift
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
view.backgroundColor = UIColor.white

/// Instantiate StackView and configure it
let stackView = UIStackView(frame: .zero)
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fill
stackView.spacing = 20
stackView.translatesAutoresizingMaskIntoConstraints = false

view.addSubview(stackView)

/// Setup StackView's constraints to its superview
view.topAnchor.constraint(equalTo: stackView.topAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: stackView.bottomAnchor).isActive = true
view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor).isActive = true
view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor).isActive = true


/// Add subviews
let switchh = UISwitch()
switchh.isOn = true
switchh.backgroundColor = UIColor.cyan
stackView.addArrangedSubview(switchh)

let label = UILabel()
label.backgroundColor = UIColor.magenta
label.text = "label"
stackView.addArrangedSubview(label)

let button = UIButton(type: .infoDark)
button.backgroundColor = UIColor.yellow
stackView.addArrangedSubview(button)
```

# その他
サブビューの `isHidden` プロパティに対応しています。
`isHidden` を `false` → `true` に変えるだけでStackView上から非表示にでき、かつ他のサブビューが再レイアウトされます。

# 所感
簡単で使いやすいとは昨年から見聞きしてましたが、全くその通りでした。
大抵のレイアウトはUIStackViewで実装できそうな気がします。
iOS8のサポートを打ち切ったプロジェクトでは使わない理由はありませんね。

---

# Credit（画像アイコン）
- <div>Icons made by <a href="http://www.freepik.com" title="Freepik">Freepik</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>
- <div>Icons made by <a href="http://www.flaticon.com/authors/tae-s-yang" title="Tae S Yang">Tae S Yang</a> from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a> is licensed by <a href="http://creativecommons.org/licenses/by/3.0/" title="Creative Commons BY 3.0" target="_blank">CC 3.0 BY</a></div>