UIViewControllerのレイアウトに関する処理

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

## 参考文献

この記事は以下の書籍の情報を参考にして執筆しました。3章を読んでの内容になります。

- [よくわかるAuto Layout iOSレスポンシブデザインをマスター](https://www.amazon.co.jp/%E3%82%88%E3%81%8F%E3%82%8F%E3%81%8B%E3%82%8BAuto-Layout-iOS%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B7%E3%83%96%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%82%92%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC-%E5%B7%9D%E9%82%89-%E9%9B%84%E4%BB%8B/dp/4865940324/)

#UIViewControllerとレイアウトをサポートするクラス
##UIViewにおけるレイアウトのサイクル

制約の更新
フレームの更新
レンダリング

の３ステップ。

###制約の更新
制約が変更されるとレイアウトにおけるコンポーネントの位置関係が更新されるので、レイアウトエンジンがその条件を満たすための再計算を行う。
UIViewのupdateConstrains()が呼ばれ子から親ビュー(ボトムアップ)に制約の計算が行われる。

制約の更新の条件
・制約のacticeフラグによる有効・無効化。
・制約の優先度変更
・制約の変更・追加・削除。
・制約を与えられたビューの階層変更。

明示的に制約を更新する方法
・self.updateConstraintsIfNeed()を呼ぶ。呼んだ瞬間更新される。
・setNeedUpdateConstraints()を呼ぶ。制約を更新する必要があるフラグが立ち、次のレイアウトパスで再計算される。


###フレームの更新
制約情報が更新されると、レイアウトを更新するためにレイアウトエンジンが計算したフレーム情報をビューが受け取り、layoutSubviews()が呼ばれ、親から子(トップダウンに)にフレームの更新が実施される。またlayoutSubviews()が呼ばれるとupdateConstraintsIfNeedが呼ばれる。

フレーム更新を引き起こす条件の例
・ビューのフレームの変更。
・サービューの追加・削除。
・UIScrollviewのサブクラスにおいてcontentOfSetが変更された時。

明示的にフレームを更新する方法
・self.layoutIfneeded()。
呼んだ瞬間更新される。
・setNeedsLayout()を呼ぶ。フレームを更新する必要があるフラグが立ち、次のレイアウトパスで再計算される。

###レンダリング
更新されたフレーム情報をレンダリングする。ディスプレイに変更を表示するためにdrawRectメソッドが呼ばれる。

レンダリングのタイミング
・ビューの一部を隠している別のビューの移動または削除。
・hiddenになっていたビューの再表示。
・ビューの画面外までスクロールし、再び画面内に戻す。
・ビューのsetNeedDisplayメソッドまたはsetNeedsDisplayInRect(_ :)メソッドの明示的な呼び出し。

##ViewControllerのライフサイクル

ViewController生成
loadView()
viewDidLoad()
viewWillAppear()
viewWillLayoutSubviews()
viewDidLayoutSubviews()
viewDidAppear()
表示完了
viewillDisappear()
viewDidDisappear()
ViewController解放

###loadView()
ビューをメモリに読み込み、対象のポインタと関連付けを行うメソッド。
storyboardが存在する場合、storyboardで定義されたビューがセットされる。
storyboardを用いずにカスタムビューを定義する場合はloadviewをオーバーライドし、設定を行う必要がある。

```swift
  override func loadView() {
    // 親ビューの定義
    let contentView = UIView(frame: UIView().bounds)
    contentView.backgroundColor = UIColor.black
    self.view = contentView
    
    // addSubViewでボタン追加とか制約追加したり
 }
```

###viewDidLoad
loadViewが完了した時に呼ばれる。
loadViewで読み込みが完了したオブジェクトにプロパティをセットしたり、クラス内で用いるオブジェクトの初期化をする。

###viewWillAppear()
ビューが表示される直前に呼ばれる。
バックグラウンドからアプリを開き直したり、UITabBarControllerにおけるタブの切り替え時も呼ばれる。
この段階ではユーザにUIは提供されていないので計算コストが高い処理は避ける。

###viewWillLayoutSubView()
ビューのレイアウト開始時に呼ばれる。
ビューコントローラが読み込まれた時だけでなく、端末の回転やビューの再表示によってビューが新しい大きさになった時に必ず呼ばれる。

viewWillLayoutSubView()が呼ばれた後、ビューコントローラーのレイアウトが開始される。
このタイミングで、UIViewのレイアウトサイクルでいう制約の更新、フレームの更新が行われる。

###viewDidLayoutSubviews()
ビューのレイアウトが完了した時に呼ばれる。
この時点で、UIViewのレイアウトサイクルでいうフレームの更新が行われているので、ビューコントローラーの持つビュー内のレイアウトが確定する。

###viewDidAppear()
ビューが表示された直後に呼ばれる。
この時点で、UIViewのレイアウトサイクルでいうレンダリングが行われている。
UIがユーザに提供されているのでUXに直接影響する処理はしない。
viewWillAppear()と対になっているため、バックグラウンドから復帰したり、UITabBarControllerにおけるタブの切り替え時も呼ばれる。

###viewillDisappear
ビューが階層から削除されたときに呼び出される。
このメソッドをオーバーライドしてビューの非表示、非表示に関する追加の処理を実行できる。

###viewDidDisappear
ビューが階層から削除される前に呼び出される「。
このメソッドをオーバーライドして編集の変更をコミットしたり。削除に関連する処理を実行できる。


##UIWindow
UIWindow : ディスプレイにおけるウィンドウの役割を担っている。
例えばキーボードやUIAlertViewは新しいウィンドウを生成して必要な部品を生成している。

keyboard表示時にデバックした時の画面

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/8ee3d0ef-61d8-9ce8-e0a0-45566fa22bb3.png)



###Windowの重なり順
UIWindowのwindowLevelプロパティの値が大きいものが上に表示される。
UIKitではNormal,Alert,StatusBarの３種類が定義されており、アラートの値が一番大きい。

```swift
UIWindowLevelNormal == 0.0
UIWindowLevelAlert == 2000.0
UIWindowLevelStatusBar == 1000.0
```

###Windowの画面サイズ

UIScreenサイズを取得する方法
この方法で画面のサイズは取得できる。

```swift
    let screenSize = UIScreen.main.bounds
```

しかしiOS9.0からiPadにマルチタスク機能が搭載されたので、スクリーンサイズとウィンドウサイズが違う場合がでてきた。
よってWindowのサイズを取得する事で正確な値をとる。

```swift
    let winodwSize = UIApplication.shared.keyWindow?.bounds
```

iPadでマルチタスクにしたとき、オレンジの枠がscreenSizeで赤の枠がwindowSizeとなる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/3b899811-7782-2676-144c-c6ae08a42151.png)


##UIStackViewとAutoLayout
UIStackViewの中にオブジェクトを並べると自動で並べてくれるが、このオブジェクトには自動的に制約が付与されている。

stackViewにのみ制約をつけて、中にLabel、UIimage、UITextViewを入れる。
表示する画像を決めると中のオブジェクトの制約は自動的に決まる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/a62dcf4d-b5a3-1598-e9b3-ace08c57967a.png)



オブジェクトの配置方法はUIStackViewのプロパティによって決まる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/065ba0f7-5c95-b7c0-f5ce-c6e268b2792f.png)

Spacing : オブジェクト間の距離
Axis : 並べる方向。水平か垂直。
Alignment : 揃え方。軸方向での中央揃え、左揃え、右揃え、大きさを最大にして並べる(Fill)。
(下記の例では垂直方向の例しか取り扱っていないが水平方向の場合そのまま横にしてみたときと同じ)

Fillの場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/b7e26da9-8952-628b-8e0f-a8d8ce42001e.png)


Distribution : 並べ方

Fill : Intrinsic Content Sizeを満たしながら、軸方向にサブビューを並べる。(コンテンツの大きさに合わせて幅が決まる)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/2bea0bac-4979-d514-e412-6499ff3fe873.png)


Fill Equally : 軸方向に均等に高さを指定して並べる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/c8c47e86-cf14-7b0b-ba4f-894f0b4fc15e.png)

Fill Proportionaally : Intrinsic Content Sizeの比率に合わせた高さで、軸方向にサブビューを並べる。(コンテンツの比率に合わせて幅が決まる)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/b6a50a52-7e0f-fc59-dd73-5bfbd5e50163.png)


Equal Spacing : Intrinsic Content Sizeを変更せずに、軸方向におけるサブビューのスペースを均一にして並べる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/66e17819-eeb7-9ea3-64e9-75e324d1560d.png)

Equal Centering : Intrinsic Content Sizeの大きさのサブビューたちの中心距離が均一になるように並べる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/221ceddf-da7e-ce7a-001f-8362809bdb94.png)


FillとFill Proportionaallyの違い
stackViewの高さをコンテンツが入らない範囲で固定にすると、Fillの方は高さの制約が必要になるのに対して、Fill Proportionaallyは比率に合わせてコンテンツを小さくした。

Fill
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/97605ba0-d741-1926-78f1-cea5bee5013a.png)
Fill Proportionaally
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/8bc4be25-6eaf-c5af-e9d3-93c09aa0d5c7.png)