怖くない！AutoLayout 〜多画面対応 with Storyboard〜


# AutoLayoutとは
AutoLayoutはiPhone SE, iPhone 7, iPhone 7Plusなど、様々なサイズのiPhone全てでいい感じにパーツを配置するための仕組みです。
Storyboard上でパーツを配置するときに、絶対座標で並べるのではなく、「上端を揃える」「y軸中心から10px」「このパーツとこのパーツは等幅」のような、制約(Constraints)によって、自動で位置を計算することで実現します。

昔はiPhoneのサイズはバリエーションが少なく、個人アプリぐらいならStoryboardで適当に配置していても許されていましたかもしれませんが（？）、最近はバリエーションも多いので、リリースするにはAutoLayoutはほぼ必須になっています。
特に、iPhone X, XS, XRなどはノッチ（切り込み）があり、SafeArea等に対応するためには必須です。

### 自動でもできるけど

AutoLayout使ってると言っても、「逃げるは恥だが役に立つ！」と言ったりして、適当に並べてから `Add missing Constraints` を押していませんか？
自動で追加されるものは、一見その端末ではうまくいってるような気もしますが、違う端末の場合とか、余白を少し調整したいときとか、非常にメンテナンスしにくいものになってしまいます。

# いろいろな束縛方法
束縛を行うには、いろいろな方法が用意されています。Storyboardの右下のボタンポチポチするとでてきます。これらのボタンですね。

![DiariesTableViewCell_xib.png](https://qiita-image-store.s3.amazonaws.com/0/77778/985bf031-c386-3bd5-4e2b-6e6896bc0ce2.png "DiariesTableViewCell_xib.png")

いろいろな束縛方法がありますが、自分的に一番わかりやすくてやりやすいと思う、`Spacing to nearest neighbor` を中心に、束縛方法を紹介します

#### 絶対値での束縛

|名前|本記事内表記|内容|
|--:|:--|:--|
|width|幅固定|固定の幅、不等号にもできる
|height|高さ固定|固定の高さ、不等号にもできる
|Aspect ratio|アスペクト比|アスペクト比

#### 他要素との兼ね合いでの束縛

|名前|本記事内表記|内容|
|--:|:--|:--|
|Equal Widths|等幅|等幅|
|Equal Heights|等高|等高|
|leading edge|左端揃え|左端揃え
|trailing edge|右端揃え|右端揃え
|top edge|上端揃え|上端揃え
|bottom edge|下端揃え|下端揃え
|Horizontal Centers|横中央揃え|横中央揃え
|Vertical Centers|縦中央揃え|縦中央揃え
|Spacing to nearest neighbor|余白|隣接アイテムとの相対距離（上下左右それぞれ）
|Horizontaly in container|X軸中心|親要素のX軸中心からの距離
|Vertically in container|Y軸中心|親要素のY軸中心からの距離

`Spacing to nearest neighbor` はこんな感じです。

![スクリーンショット_2017_02_28_22_39.png](https://qiita-image-store.s3.amazonaws.com/0/77778/2218a7ac-6c4f-2227-30f8-027f7d3939c1.png "スクリーンショット_2017_02_28_22_39.png")

余白は数字を入れて、濃い赤線でつながれた部分のみに制約が追加されます。

widthなどもこの画面から、チェックを入れると追加できます。

# 束縛対象を意識する
Viewのレイアウトを考える時、それぞれの要素に対して、

- x
- y
- width
- height

の４つが、一意に決まるようにルールを作ります。これを意識しながら、想像して、制約を追加します

例えば、固定幅100という制約はwidth=100、アスペクト比の制約はwidth = K * height、のように、方程式で表すことができます。
4つの値がわからない、4次方程式の解が定まるには、基本的には4つの制約が必要になりますので、基本的に制約は4つがメンテもしやすくてよいです。
5個以上制約がある場合は、冗長な制約はないか、確認してみてください。冗長な制約があると、矛盾を生みやすいし、後からの調整もしにくくなります！

※ これめっちゃ大事です！！

冗長でもなく4つ以上の制約が付く場合は、不等号制約と、優先度を考えた制約です。

例えばエディタを作る時、文字の内容に応じてのみサイズを変えていると、文字がない時に高さが0になってしまって、入力開始したくてもタップが反応しません。
そこで、最小の高さを大きめに設定したりします。
でも文字を入力していったあとは、それに合わせて長くします。

不等号制約は、一旦固定幅などの制約をつけた後、右側のメニューから = を > にすると設定できます。

他にも、4つ以上の制約をつけた上でプライオリティを設定して、より複雑な制御を行うこともあります。

### 想像クイズ
例えば、画面に対して「X軸方向に中心」「高さを固定」「幅固定」という制約があったら、束縛できているでしょうか？

正解はNOです。
後ろ２つで、width, heightは固定されています。
画面のサイズは当然決まっているのと、幅が固定かつX軸方向に中心なので、x座標は束縛されています。
しかし、y座標は動いても制約は満たされますので、y座標が束縛されていません。

よって、y座標を束縛する制約を付け加える必要があります。
「`Spacing to nearest neighbor`のleading」や、「Y軸中心」などを付け加えると、これを満たすことができて、束縛成功になります。

# トップダウン、ボトムアップ
束縛する際、まず子要素に合わせてボトムアップに決めていくか、親要素に合わせてトップダウンに決めていくか決める必要があります。

１画面の中にきれいに配置したい場合や、固定サイズのセル・ビューの中に配置したい場合は、トップダウンに外側から、
文章の長さや画像サイズなどによって、セルやスクロールViewの中身の高さなどを自動的に調整する場合などはボトムアップに、それぞれの高さから全体を組み上げていくように、
決めていきます。

# トップダウンに決めていく場合
トップダウンに決めていくには、親ビューが、
画面全体である（画面のサイズは決まっている）とか、
あるサイズの固定されたUIViewの中（例えばセルのサイズが固定のtableView）であるとか、
何かしらサイズが固定されている必要があります。

この場合、各Viewについて、どこかの端から順番に固定していきます。

## 画面いっぱいに広げる
一番簡単なのは、画面いっぱいに広がるViewです。背景のViewなどに便利です。

`Spacing to nearest neighbor` をすべて0にすると固定できます。
もしくはAlign edgesを４つ全てに0で設定する。
(どっちでも良いと思います)

## 具体的な例

![DiariesTableViewCell_xib_—_Edited.png](https://qiita-image-store.s3.amazonaws.com/0/77778/84ae4891-d7b3-c814-896d-d98c06921cd2.png "DiariesTableViewCell_xib_—_Edited.png")

セルのサイズは決まっているものとして、上の例のように固定してみます。数字の背景のビューから固定してみましょう

### 日付の背景の青いViewの固定

まず位置について、
x軸方向に関しては左のスペースを0にすると固定できそうです。
y軸方向に関しては、上下のスペースを設定するか、中央にするかで固定できそうです。

サイズについては、子要素にラベルがあるので幅が小さすぎると困るため、幅を固定したいです。
高さはセルの高さを超えないようにしたいですね。

よって、「左余白固定0」「幅固定」「上余白固定10」「下余白固定10」とすることで、背景のビューが固定できますね。

### 数字、曜日のラベル固定
数字の背景は既に固定されているので、数字の背景の中での、数字・曜日の位置・サイズを束縛します。

ラベルの数字がはみ出すと困るので、はみ出さない高さに固定します。

どちらもX軸中心にしたいですね。もしくは、両サイドの余白を0にした上で、labelのtextAlignmentをcenterにすることで中心にするほうが考えやすいかもしれません（X軸中心のときは幅の制約が必要）

あとはy位置は、これも趣味ですが、自分ならそれぞれY軸中心-5, Y軸中心+15のような感じで固定します。もちろん上下余白で制約つけても構いません

### ラベル固定

まず、本文とタイトルの２つのラベルは左端、右端ともに揃えたほうがきれいだと思うので、左端揃え、右端揃えの制約をつけます。
その後、どちらかに左余白、右余白の制約を入れて、両方の左右の余白を決定します（こうすることで、それぞれに余白を設定したときよりも、余白を変更したいと思ったときに一箇所変更するだけで両方に反映される）

タイトルのラベルは上からの余白で決定し、必ず一行なので高さも固定してしまいましょう。
本文は複数行の可能性もあるので、上余白（タイトルラベルの下端からの距離になる）と下余白を決定することで、残りの領域すべて本文のラベルにしてしまいます！

### まとめ
これですべてのレイアウトが決定されました！必要な制約は、4*5(パーツ数)=20こでしたね！

ルールを重要な順に考えて、そしてさらに束縛対象を意識して、どういう制約(横方向のことなのか縦方向のことなのか、など)をつければ良いのか考えて、それを追加することで、きれいに制約を追加します！

# ボトムアップに決めていく場合
親要素のサイズが定まっていない時、すなわち
ScrollViewのスクロール領域（ContentView）のサイズを子要素のサイズから確定させていく時や、
テーブルのセルのサイズを中身によって変えたりする時（Twitterアプリのような）は、ボトムアップに決めていく必要があります。

# intrinsticContentSize
Viewによっては、固有サイズを持っているものがあります。

画像のセットされたUIImageViewや、
テキストがセットされてかつスクロールの禁止されたUITextView、
UILabelなどは、 コンテンツに応じて推奨されるサイズを計算することができます。

参考: [Content Hugging Priority](https://developer.apple.com/documentation/uikit/uiview/1622485-setcontenthuggingpriority), [Content Compression Registance Priority](https://developer.apple.com/documentation/uikit/uiview/1622526-setcontentcompressionresistancep)

つまり、画像のアスペクト比固定であったり、テキストがちょうど収まるサイズであったりになるようにできます。

これもしくは幅固定などの制約を使うことで、うまくボトムアップに外側のレイアウトを束縛します。

例えば、テキスト全体が埋まるようなセルを作る場合には、
まずTextViewのscrolling Enabledのチェックを外し、
TextViewの左右余白を0にするなどで幅を固定して、上下余白も0にすることで、セルサイズもTextViewのpreferredContentSizeと同じにすれば実現できる。
上下にマージンを入れたければ上下余白を0でなくて10とかにすればよいですね。
（セルの横幅はTableViewと同じなので固定されている）

View固有のサイズはもともと決められているものもあれば、自分で計算することもできます。
自分で作ったクラスについて `var intrinsicContentSize: CGSize { CGSize(width: 100, height: 100) }` のように返すと実現できます。

コンポーネントの内部に閉じられるので、この方法をよく使っています。

参考：[intrinsicContentSize](https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize)

# AutomaticDimention
~~tableViewのセルは、デフォルトでは高さは固定なので、ボトムアップに設定するには、viewDidLoadで、tableViewのrowHeightを以下のように設定しておく~~

~~`tableView.rowHeight = UITableViewAutomaticDimension`~~

※ デフォルトになったので設定不要になりました。設定しても問題はないです。

# ScrollViewのAutoLayout
ScrollViewを使っている場合のAutoLayoutはやや特殊です。

UIScrollView自体のサイズは、トップダウンに決める必要があります。
大抵の場合、上下左右余白をすべて0にして、画面いっぱいに広げる事が多いと思います。

scrollViewの中身のサイズは必ずボトムアップに決める必要があります。

scrollViewはそのままでは横スクロールにも対応しているので、ContentViewとしてUIViewを置き、そのUIViewの幅はUIScrollViewと等幅の制約を加えることが多いです。
（横スクロールを禁止する）

そしてそのあと、ボトムアップにUIViewの "高さ" を計算させることが多いです
（もちろん、レイアウトが固定の場合はUIViewに高さ固定の制約を加えて、そこからトップダウンにレイアウトしても問題ありませんが、将来的な要素の増減を考えると、ボトムアップで決めたほうがいいかな、と思います）

### Adjust ScrollView Insets

今この文脈であまり関係ないが、よく陥る罠。

実はデフォルトからチェックが入っているもので、ScrollViewやTableViewなどは、navigationBarやTabBarで隠れる分インセット（padding）を追加する、というものである。
意味を知らないと、navBarと重ならないようにtableViewをおいたのになぜか上に余白ができる〜と悩んでしまう

意識しないとはまる。

# 関連付け

各制約も、Swiftのコードに対して関連付けさせることができます。

たとえば、固定幅の制約をコードから動的に書き換えてレイアウトを再計算させる、なんてこともできます

# 上下左右余白設定の問題
上下左右余白の設定は、html, cssのmarginのように自動的に近いものに対して適用されるのではなく、「その制約を入れたときに横にあったもの」に対して適用される。
よって、パーツを追加したり削除したりすると束縛をし直さなければいけないし、また動的に変わる場合などは束縛できない

### 動的に子要素の数が変わる場合

- UIStackViewの導入を検討する
- 幅の制約を1にすることでごまかす（裏技）
- Priorityを適切に設定する

UIStackViewは子要素を自動的にリサイズするもので、上手く使うととても便利。
裏技として、幅を1にしてごまかすことで、小要素の数を変わったように見せかけて自動でレイアウトかけることができる。

関連付けを行って、高さを1にすることで(0にするとなぜか次200とかにしても表示されない)

# Priority

各制約にはPriority（優先度）があります。
contentSizeなども実際はpriorityの低い制約、という扱いになっています。
オブジェクトや縦横、伸びるか縮むかによりデフォルト値が違いますが、250〜751が設定されています。

手動で追加した場合、defaultではpriorityは1000（最高）になっています。

これを調整することで、隠したときなどにも適切にレイアウトすることができます。

# UIStackViewについて

別の記事に書きました！のでこちらも合わせて読んでみてください！

実際の開発ではバージョンアップで要素が変わったりすること多いので、自分の場合はほとんどUIStackViewを使って開発しています。
ですが、基礎になっているのはあくまで通常のAutoLayoutなので、これを理解しておくとよりStackViewを使いこなせると思います！

[普通の画面をUIStackViewでよりロバストに](https://qiita.com/_ha1f/items/3b3c70a84263aca210ad)