Interface BuilderにおけるAuto Layoutのテクニック[WWDC 2017]

WWDC2017のAuto Layout Techniques in Interface Builderのセッションのまとめになります。これはAutoLayoutを扱う上での6つのテクニックについて紹介するセッションです。この記事ではその概要と手元で検証した様子を紹介しようと思います。検証したコードはセッション中のコードと同一のものではないのでご了承ください。

* [動画](https://developer.apple.com/videos/play/wwdc2017/412/)
* [スライド](https://devstreaming-cdn.apple.com/videos/wwdc/2017/412icy0vh6ays/412/412_auto_layout_techniques_in_interface_builder.pdf)
* [試しに作ったサンプルコード](https://github.com/akatsuki174/AutoLayoutTechniques)

※サンプルコードはもう少し付け足したり直していく予定です。

# Changing layout at runtime

アプリ実行時に動的にレイアウトを変えたい時にどうするかという話です。制約を都合よく扱えるようにするために調整したい要素を上に乗せたviewを1枚作成してその制約及び関係する制約のisActiveフラグを切り替えることで思うようなレイアウトを実現するという手法が紹介されていました。一部を抜粋するとこんなかんじです。詳しいコードはサンプルコードを参照してください。

```swift
if shouldShow {
    zeroHeightConstraint.isActive = false
    edgeConstraint.isActive = true
} else {
    edgeConstraint.isActive = false
    zeroHeightConstraint.isActive = true
}
```

この方法自体はまあよくある話かなーと思ったので詳細は省略します。一つ、全然違うところで勉強になったのはweakのNSLayoutConstraintを一度isActive = falseにすると制約がnilになってしまうということです。この件に関しては以下のstackoverflowを参考にしました。

* [Why weak IBOutlet NSLayoutConstraint turns to nil when I make it inactive?](https://stackoverflow.com/questions/38051879/why-weak-iboutlet-nslayoutconstraint-turns-to-nil-when-i-make-it-inactive)
* [When can I activate/deactivate layout constraints?](https://stackoverflow.com/questions/27494542/when-can-i-activate-deactivate-layout-constraints)

# Tracking touch

タッチを追跡する方法についてです。一般にviewの位置はframeによって定義され、制約が付いている場合はframeの値は制約から計算されます。しかしviewの位置に影響を与えるものとしてtransformプロパティがあります。これは制約からframeが計算された後に拡大、回転などを追加することができます。

デモでは左右にカードをポイするようなUIを例にして説明されています。第一段階としてpanジェスチャーをした時にviewが移動する（ただし手を離してもviewが元の位置に戻らない）、第二段階として手を離した時にviewが元の位置に戻るようにする、第三段階としてviewが元の位置に戻る時にバウンドするようなアニメーションを付けるということをしていました。

まとめとしては、viewの位置は複数のプロパティの組み合わせだということ、今回楽しいインタラクションを与えたのはたった数行のコードだということ、一時的な変更にはtransformが便利ということあたりでしょうか。こちらも詳しくはサンプルコードを参照してください。ピンク色のviewが動くようになっています。

# Dynamic type

Dynamic typeは一連のテキストスタイルを提供するもので、ユーザは見出し、本文などのスタイルのサイズをコントロールすることができます。作り方によってはテキストは大きくなるがレイアウトが適切ではなく、読みにくいなどの状況が発生します。幸い、Interface Builderを使えば簡単に修正ができます。

UILabelを選択した状態で右ペインのDynamic Typeのチェックボックスにチェックを付けます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/100736/b1e3ef2f-c66d-f20b-64f1-59577d6a2670.png" width=90%>
このプロパティを有効にするには、固定のフォントサイズではなくテキストスタイルを指定する必要があります。Fontプロパティの「T」ボタンをクリックしてSystemのものではなくText Stylesのどれかを選択します。
<img src="https://qiita-image-store.s3.amazonaws.com/0/100736/24acd360-45fc-6feb-74d1-2e417e0951cf.png" width=90%>

※補足：コードで指定する場合の最低限の実装例は以下の通りです。

```swift
if #available(iOS 10.0, *) {
    dynamicTypeLabel.adjustsFontForContentSizeCategory = true
    dynamicTypeLabel.font = UIFont.preferredFont(forTextStyle: .headline)
} else {
    NotificationCenter.default.addObserver(forName: NSNotification.Name.UIContentSizeCategoryDidChange, object: nil, queue: nil, using: { _ in
        self.dynamicTypeLabel.font = UIFont.preferredFont(forTextStyle: .headline)
    })
}
```

実機（iOS 10.2.1）で確認したらこんなかんじになりました。
※補足：iOS 9以前ではフォントサイズの変更を監視するコードを追加しなければならないようです。

| デフォルトサイズ | 最大サイズ |
|:-:|:-:|
| <img width="280" alt="IMG_1371.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/f2a49360-bc13-bdd2-e5ad-ef67c779fa7e.png">  | <img width="280" alt="IMG_1374.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/163fce07-f946-7cad-21b1-4cb7e63aea8a.png">  |

※補足：設定アプリで文字サイズを変更する方法
<img width="280" alt="IMG_1372.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/661d0695-7db3-701e-f3ac-7915df84d873.png"><img width="280" alt="IMG_1373.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/3eedfb19-7d65-35b5-b6a8-e97c7b58b4e3.png"><img width="280" alt="IMG_1375.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/c240dadd-87f2-829f-8003-6cc02378ba44.png">

シミュレータの場合、設定アプリを起動するのは面倒なので別の方法で文字サイズを変更します。Depeloper ToolからAccessibillity Inspectorを起動します。
<img width="320" alt="IMG_1374.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/3b49ae19-b739-fb58-33a2-ee368e9c373e.png">
するとこんな画面が現れるのでSimulatorをターゲットにします（実機を繋いでいる時はシミュレータはターゲットに出てきませんでした。ターゲットを実機にすれば実機のフォントサイズもここで変えられます）。
<img width="280" alt="IMG_1374.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/aa60a750-c346-4a9f-8788-f8a72c2c4052.png">
右上の設定ボタンをクリックするとスライダーが出てきて、シミュレータを横に並べつつ表示を確認するということができます。

| 最小サイズ | 最大サイズ |
|:-:|:-:|
| <img width="450" alt="IMG_1374.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/6e527cc1-6a22-d4f9-0dae-5f65aecc04b7.png">  | <img width="450" alt="IMG_1374.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/9b19c150-051c-6b19-5f54-de8620846a63.png">  |

このように便利にフォントサイズを変更することができますが、大きくしすぎるとラベルが重なり合ってしまうので今度はそれを修正します。

| デフォルトサイズ | 最大サイズ |
|:-:|:-:|
| <img width="280" alt="IMG_1377.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/02e07c83-fb45-d08d-3a82-688ecf5cbbc6.png">  | <img width="280" alt="IMG_1376.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/badaf1a9-0e06-0378-0f39-e6860d5a4c7e.png">  |

今2つのラベルが乗っているviewには高さの制約が付いています。
<img width=90% alt="IMG_1376.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/895daf5d-a817-7c9b-666e-a865f616be60.png">
これを、高さの制約の代わりにtop, bottom, 2つのラベルの間の制約を使うことで調整します。
2つのラベル間の制約は新しくできたVertical Baseline Standard Spacingを用いることができます。これはラベルのText Styleとフォントサイズに応じて制約の定数を調節すること以外はVertical Spacingと変わりません。
<img width="280" alt="スクリーンショット 2017-07-22 21.03.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/f527b5d1-91c3-35f5-f10e-96bc1f2b23a4.png">

この制約を加えてから実行すると先程より良くなっています。

| 調整前 | 調整後 |
|:-:|:-:|
| <img width="280" alt="IMG_1376.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/badaf1a9-0e06-0378-0f39-e6860d5a4c7e.png">  | <img width="280" alt="IMG_1378.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/bd92825a-f927-b25a-5438-c33f71dea578.png">  |

より詳しい情報は[Building Apps with Dynamic Type](https://developer.apple.com/videos/play/wwdc2017/245/)で得られます。

さらに大きな文字を使用する場合はアクセシビリティを利用します。
<img width="250" alt="IMG_1379.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/d81721af-55e6-1a86-6908-cf71623bb068.png"><img width="250" alt="IMG_1381.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/7003b413-433c-f873-ce11-abde86641fa5.png"><img width="250" alt="IMG_1380.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/62136a19-7d70-fd1a-4fc1-43bda24392a8.png"><img width="250" alt="IMG_1382.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/b3b93b93-a9e1-55a4-a2ae-02d2e7c9edbe.png">

# Safe areas

iOS 11からtop及びbottom layout guideがdeprecatedになって新しくUIViewのプロパティとしてSafe Area Layout Guideというものができます。これによりナビバー、タブバーの中央に配置するなどの操作がしやすくなります。高さを変えたりlandscapeを変えたりしてもそれに応じて配置してくれます。Safe Area Layout GuideはtvOSでも適用することができます。考慮しなければならないことはディスプレイ幅のバリエーションが増えるということと、オーバースキャンに関してです。

Safe Area Layout Guideを有効にする方法は簡単で、file inspectorでUse Safe Area Layout Guidesのチェックボックスにチェックを入れるだけです。しかも後方互換性があります。
<img width="300" alt="スクリーンショット 2017-07-22 22.40.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/c6fb913b-600b-4ace-458e-11d3720ef59b.png">　<img width="300" alt="スクリーンショット 2017-07-22 22.41.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/9b07901c-9523-c6c6-81d6-74f93cda7288.png">

ここから先は[こちらの記事](https://useyourloaf.com/blog/safe-area-layout-guide/)を参考にしつつ自分でも検証してみた内容になります。

コードでSafe Area Layout Guideを設定する場合は以下のようになります。

```swift
private let sampleView = UIView()
    
override func viewDidLoad() {
    super.viewDidLoad()
    setupView()
}
    
private func setupView() {
    sampleView.translatesAutoresizingMaskIntoConstraints = false
    sampleView.backgroundColor = .brown
    view.addSubview(sampleView)
        
    let margins = view.layoutMarginsGuide
    NSLayoutConstraint.activate([
        sampleView.leadingAnchor.constraint(equalTo: margins.leadingAnchor),
        sampleView.trailingAnchor.constraint(equalTo: margins.trailingAnchor)
    ])
        
    if #available(iOS 11, *) {
        let guide = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([                
            sampleView.topAnchor.constraintEqualToSystemSpacingBelow(guide.topAnchor, multiplier: 1.0),
            guide.bottomAnchor.constraintEqualToSystemSpacingBelow(sampleView.bottomAnchor, multiplier: 1.0)
        ])
    } else {
        let standardSpacing: CGFloat = 8.0
        NSLayoutConstraint.activate([
            sampleView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor, constant: standardSpacing),
            bottomLayoutGuide.topAnchor.constraint(equalTo: sampleView.bottomAnchor, constant: standardSpacing)
        ])
    }
}
```

<img width="280" alt="スクリーンショット 2017-07-23 9.00.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/67b09f83-b6cd-c9b4-4f96-3c819c0a5662.png">

また、記事によるとiOS 11において新しく`constraintEqualToSystemSpacingBelow`、`constraintEqualToSystemSpacingAfter`が追加されたおかげでstandard spaceをハードコードする必要がなくなったと書かれています。

ちなみにUIViewControllerの方にもsafeAreaLayoutGuideプロパティがあったのですがすでにdeprecatedになっています。上記で使っているのはUIViewのsafeAreaLayoutGuideです。

# Proportional proposition

superviewに対して相対的に位置付けする際のテクニックについての話になります。あるviewを高さ70%の大きさに縮めたい時、spacer viewを用いて調節する方法を紹介しています。

高さ70%のspacer viewを作るためにはいったんsuperviewに対してEqual Heightの制約を張ります。
<img width="280" alt="スクリーンショット 2017-07-23 10.44.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/f3b0bf62-2870-121c-cc1d-d30343d81bc8.png">
今回はsuperviewに対して70%ではなくSafe Areaに対して70%の制約を張りたいので、右ペインの制約設定でSuperview.HeightをSafe Area.Heightに変更します。
<img width="280" alt="スクリーンショット 2017-07-23 10.47.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/2546c8c7-9b8a-6bfc-613a-fe60b05c761d.png">
最終的な制約の状況は次の通りです。
<img width="280" alt="スクリーンショット 2017-07-24 11.41.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/a5d197b9-b1b2-0e6c-e86a-10490eba867d.png">
結果的に端末を変えても、縦横を変えても70%という比率を守ったレイアウトができます。
<img width="280" alt="スクリーンショット 2017-07-24 12.08.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/5979f6ca-2880-b297-c051-576a9beb2e8e.png">　<img width="300" alt="スクリーンショット 2017-07-24 12.09.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/8a709f8c-6b31-52fc-bfcd-ecd4a99043fd.png">　<img width="400" alt="スクリーンショット 2017-07-24 12.19.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/930b3084-159f-f433-856d-cc11ad54382f.png">

もしInterface Builderではなくコードで同様のことをしたい場合はUILayout Guideを使います。

# Stack view adaptive layout

stack viewを用いて理想に合ったレイアウトを作成していく方法についてです。今回の例では縦と横でレイアウトを変える実装をします。
<img width="280" alt="IMG_1407.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/925bc05a-85e1-a9cf-57cd-76a7a40a8f0a.png">　<img width="400" alt="IMG_1407.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/100736/bd18f927-9316-ab99-2e45-3641f9b73ebf.png">

細かいところは省いてかいつまんで説明します。

まずは以下のように準備をします。stack viewの中身は等間隔にしたいのでDistributionをFill Equallyにしています。
<img width="400" alt="スクリーンショット 2017-07-24 15.23.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/6df4a1cf-f610-d19c-baef-14fbac2fe9d3.png">

ここから自分が望むレイアウトになるようにカスタマイズします。Xcode9, iOS 11からは標準の間隔オプションがあるのでそれを選択してみます。
<img width="400" alt="スクリーンショット 2017-07-24 16.05.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/b1858dce-5ea5-f2c5-5533-b897c8219ab5.png">

次にview4つを正方形にします。あるviewから自身に対しAspect Ratioの制約をかけます。
<img width="280" alt="スクリーンショット 2017-07-24 16.26.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/7d09ee8c-6d95-a6d0-9a5a-444deebd023b.png">
ただしこのままだと制約エラーがおきます。試しにinstalledプロパティのチェックを外すとエラーはなくなりますがsuper viewにフィットしなくなります。
<img width="280" alt="スクリーンショット 2017-07-24 16.40.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/c6ff9e2f-5a8c-8161-7611-b31162734917.png">　<img width="280" alt="スクリーンショット 2017-07-24 16.40.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/c64ac478-6d0a-ce59-3485-a831d8e459b8.png">
そこでtop、trailingの制約をON/OFFしてみます。OFFにすることで制約エラーはなくなり4つのviewは正方形になりますが、やはりsuper viewにフィットしない状態（余白がおかしいなど）になってしまいます。
ここで、viewを正方形にすることとFill Equallyを同時に満たすことができないのだということがわかります。今回はsuper viewの全体を活用する必要はないのでbottomの制約を外すことによってエラーを解決することはできますが、landscapeのときのことを考えGreater Than or Equalにしておきます。

ただしこれだけだとlandscapeにした時にエラーが発生するので修正していきます。
Xcode9で新しく登場したサイズクラスのHiddenプロパティを活用します。stack viewでhiddenプロパティを使うとレイアウトは崩れるものの配置はキープできます。
<img width="400" alt="スクリーンショット 2017-07-24 19.42.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/69362195-dcfc-6637-b394-61ef74555df0.png">
HeightがCompactの時にはUILabelをHiddenにする設定をします。
<img width="400![undefined]()
" alt="スクリーンショット 2017-07-24 20.07.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/32deabce-09fb-8b44-4d9b-043dab8508fd.png">
するとlandscapeの時はUILabelが見えなくなります。あとは制約を修正したり、上記と同じ要領でlandscapeの時だけ右側にtext viewを表示するようにしていきます。
<img width="400" alt="スクリーンショット 2017-07-25 15.05.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/100736/239ea14a-08ce-495b-5a74-9eff1011ae85.png">

まとめとしては、使えるときはalignment, distribution, spacingを駆使してstack viewを使って使うと良い、stack viewを使うと少ない制約で済ませることができる、Xcode9におけるhiddenプロパティはでsize classに対応しているということです。