ios概論: View

# はじめに
<b>iosの仕組みを概要的に説明すること</b>がモチベーションです。
想定読者はios〜中級者です。
仕組みをわかって実装するための、最低限の知識が体系的に得られるといいなあと思っております。

間違っていたらご指摘していただけると嬉しいです。
また、修正・追加して欲しい内容などございましたらこれもご指摘いただけるとありがたいです。

構成は以下になります。
1.View
2.Draw
3.Animation
4.Touch

Viewの根幹となる描画機能を3章まで説明し、最後にタッチ機能を説明します。

# 全体観
UIViewとはなんでしょうか？
ドキュメントを見てみると、たくさんのクラスやプロトコルから構成されているのがわかります。

https://developer.apple.com/documentation/uikit/uiview

![スクリーンショット 2021-04-25 17.11.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252245/07216906-727f-01a6-2775-f25931ba097e.png)
これらの機能を取りまとめて管理するクラスがUIViewなのですが、
このうち根幹的な機能を担っているのは2番目のCALayerDelegateです。
つまり<b>描画機能</b>です。
画面に表示されるというのがViewとしての根幹機能であることは共感していただけるかと思います。
UIViewのほとんどメンバ変数メンバ関数が、どこに何を描画するのかに関するものでもあります。

この記事にわたって描画周りの仕組みを主に説明致します。

# 1.View
## ヒエラルキー
UIViewのヒエラルキーには”親子関係”があります。

親viewは子viewを`subviews`パラメータに格納します。
![IMG_6953.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252245/f311113e-4313-938b-39e0-44daed9566a6.jpeg)
この図の場合、①の子が②③、②の子が④です。
viewのヒエラルキーは、描画の順番に関連します。

## 描画
画面の表示は全て描画によるものです。
ざっくり説明すると、UIViewは描画キャンバスを１枚持っていて、`frame`や`backgroundColor`などのパラメータをもとにキャンバスを描画し、画面に表示させます。
UITextViewの`text`やUIImageViewの`image`なども描画パラメータの一種と考えることができるかと思います。

また、このキャンバスは実はCALayerなのですが、後でまた説明します。

親viewから順番に上書き描画されていきます。subviewsは格納が早い順に描画されます。
上の図の場合、
描画の順番は
① -> ② -> ④ -> ③
になります。

自分で描画したい場合は`draw()`をoverrideします。

## layout
1.Viewで主に説明したいのはこの章です。
少し長くなりますがご容赦ください。

### frameとbounds
`frame`が親座標系における原点と縦横幅、` bounds`が自分座標系における原点と縦横幅です。原点以外は一致します。
ただ原点は独立して変更可能です。`bounds`はどこを切り取って表示するかを決めていて、デフォルトの原点は( 0.0, 0.0 )ですが、原点を変更すると表示される矩形がその方向にずれます。
（特別な理由がなければ必要ない操作とは思いますが）

```
※画像を入れる
```

### 指定方法

layoutの指定方法は３種類あります。
1.frame直指定
2.AutoResizing
3.AutoLayout

結論から言うと、3.AutoLayoutを普段使いにして、特別な場合に1.frameを使ったほうがいいのかなと思います。
理由と合わせて各layoutの説明します。

1.frame直指定
frameの値（+回転があるなら.transform）を参考にViewの描画位置が決められるのですが、frame(x/y/w/h)を直に設定します。

2と3は制約によってframeの値を指定する方法と言えます。端末サイズや親viewのframeサイズの変化にも動的に対応できるので便利です。

2.AutoResizing
親viewに対する辺の制約と自身のサイズ(w/h)を設定できます。
子view同士の制約はできないのでちょっと不便です。
AutoLayoutがなかった時代は主流だったようです。

3.AutoLayout
AutoResizingでは設定できなかった種類の制約も付けれるようになった制約です。
AutoResizingを補完するため（？）っぽい感じで後から生まれた制約です。
参考書ではAutoLayout推奨でした。
例えば、leadingAnchorなど設定するやつですね。

AutoLayoutは、
AutoResizingの上位互換であり、
画面サイズに動的に対応できるので、異なる端末間で同じレイアウトを作りたい時、回転させたい時、などにワンソースで対応できます。この点でframe指定よりも優れています。
なので、普段使いはAutoLayoutにして、配置後のviewの位置をドラッグなどで頻繁に動かすアプリなどではframeの方がやりやすいかも、といった感じです。これが先程の理由です。

注意すべき点は、frameやAutoResizing、とAutoLayoutを併用すると、
<b>frameやAutoResizingの設定が内部で自動的にAutoLayoutに変換される</b>ことです。
結果、生のAutoLayoutとコンフリクトを起こす可能性があります。し、設定が実現すればいいやと言うスタンスで変換されるので、どんなAutoLayoutが出来上がってくるか予測がつきません。

さらに厄介なのは、<b>そのView自身はAutoLayoutを使用していなくとも、他のViewからAutoLayoutの参照にされた場合も、自動的にAutoLayoutに変換されます。<b>

これらの自動変換を止めるには、viewの`translatesAutoresizingMaskIntoConstraints`をfalseに変更します。

なので、可能な限りlayout方法の併用は避けた方が良さそうです。
意図しないAutoLayoutが発生する可能性があります。
また、自動変換を止めた場合でも、AutoLayoutと他のlayoutのどちらが優先されるのかわからないという問題もあります。（frame指定であればAutoLayoutで上書きされますが、であれば不必要にframeを指定して混乱の元にする必要はないでしょう。AutoResizingを使う理由はもはやないですね）。

### wrap_contentは実現できる
androidには、wrap_contentやmatch_parentといった便利な制約ショートカットが用意されていますが、iosにはないです。
でも、ショートカットが用意されてないだけで、iosでもほぼ任意の制約を作ることが可能です。

制約には`priority`というパラメータがあります。
制約がバッティングした時、`priority`が高い方が優先されます。

例えばwrap_contentで考えると、
親Viewのサイズを規定する制約の`priority`が低かったり、そもそも制約がなかったりする状態で、
子viewの制約が親viewに紐づいていると、子viewの制約が優先されて親viewが縮んだりします。
４辺全てにこれを適用するとwrap_contentが実現可能です。

### intrinsic content size
特定のviewクラスは`intrinsicContentSize`(本質的なサイズ)というメンバ変数を持っています。
例えば、UITextView文字列の大きさ、UIImageViewのimageの大きさなどがその本質的なサイズにあたります。
実は、デフォルトではこのサイズでviewが表示されるように制約がついています。
このデフォルト制約はpriorityが低いのですが、以下のメソッドで値の確認と上書きが可能です。
`contentHuggingPriority(for:)`
`contentCompressionResistancePriority(for:)`
`setContentCompressionResistancePriority(_:for:)`
`setContentHuggingPriority(for:)`

### その他
・xibfileからでもcodeとほぼ同等のlayoutが実装可能です。
唯一UILayoutGuideだけxibからは使用不可だそうです
これは制約用viewのようなオブジェクトで、描画を行いません。スペーサーとして無色のviewを入れるしかないときに代用すると処理量を減らせます。

・xibfileからでも端末サイズごとに制約や色、文字列などのパラメータを設定可能です。


# 2.Draw

## layer

### 全体観
iosではlayerという概念があり描画は全てlayerを通して行われます。
layerはそれぞれの描画環境（contextという）を持っていて、描画に必要な情報が全てこのcontextに渡されます。
そして、contextが描画されることで、端末画面にその結果が表示されます。

UIViewは１枚の専属layerを持っていて、`layer`プロパティから参照できます。
UIViewの描画は全てこのlayer（とそのsublayer）による描画になります。

また、UIViewのパラメータから間接的にlayerのパラメータを設定できます。
（`frame`や`backgroundColor`など）
これらの描画に必要な情報は最終的にCGContextに渡されます。
描画までの情報の流れはこんな感じ
UIView -> CALayer -> CGContext => 描画

### context
contextとは、総合描画環境のようなもので、描画を行う本体です。
描画にはcontextが必須です。

iosではCGContextクラスのインスタンスとして提供されます。
AndroidでいうCanvasです。

画材道具一式と言い換えてもいいかと思います。

contextには描画範囲と描画方法を設定できます。
デフォルトの描画範囲は、そのcontextを持つlayerの`frame`と`bounds`プロパティで定義されます。
描画方法は、CAlayerやUIViewのパラメータを通して、背景色、画像、テキスト、pathやpathの色、塗りつぶし色、cornerRadius、影、、、などが設定することで設定できます。また、draw()をオーバライドすレバカスタムもできます。

### ヒエラルキー
##### underlying layer
UIViewは`layer`プロパティにCALayerを一つ持っています。
参考書に倣ってこれをunderlying layerと呼称します。
UIViewのUIは全てこのunderlying layerの描画が司ります。

##### Viewとの関連性
underlying layerの描画順番と描画位置は、Viewの親子関係を反映します。
![IMG_6954.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252245/d80385bd-073e-f864-1023-dfd899e2a125.jpeg)
ただし、layerは`sublayers`プロパティに子layerを格納できます。
![IMG_6956.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252245/47e78a1a-f417-a9ef-a8d4-7e0ca9b8d846.jpeg)
上図の場合描画順は、1 -> 1-1 -> 1-1-1 -> 2 -> 3 -> 3-1 になります。

## カスタム描画
viewやlayerのテンプレート的な描画を超えた描画をしたいときは、draw()をオーバライドする必要があります。
draw()はlayerの描画時に呼ばれるメソッドで、このメソッドの内で、contextの設定をして、それを描画するといった流れです。

### draw()の設定方法
CAlayerのdraw()の設定方法は以下の3パターンです。
１.CALayerのサブクラスから`draw(in ctx: CGContext)`をoverrideする。
２.CALayerDelegateに設定したクラスから`draw(_ layer: CALayer, in ctx: CGContext)`を設定する。
３.UIViewのサブクラスから`draw(_ rect: CGRect)`をoverrideする。

12のパターンでは、contextがメソッドの引数として提供されます。
3のパターンでは、contextがメソッドの引数として提供されません。
これは描画方法に絡んでくるのですが、後述の描画例をみた方が早いでしょう。

Underlying layerの場合、デフォルトでUIViewがdelegateになっているので、3を使用します。
ただし注意として、生のUIViewを使用するべきです。例えば、UIImageViewのdraw()をoverrideすることは想定されていないため、描画が失敗する可能性があります。

野良CALayerの場合、１か２の方法のどちらかを選択します。

### 描画方法
描画方法は２種類あります。
Core Graphicを使う方法と、UIKitを使う方法です。

Core Graphicはiosの描画機能を全て提供します。
UIKitは多少機能が制限されますが、複数のCore Graphicメソッドをラップして簡略化して使いやすくしています。
なので、内部的にはどちらも同じことを行なっています。併用も可能です。

(個人的にはCore Graphicの方がわかりやすくて好みです。。)

各描画方法の詳しい例は他の記事に譲りますが、
基礎的な説明のために簡単な例として、実際に以下の青丸を描画してみましょう。
<img width="300" alt="スクリーンショット 2021-05-04 12.37.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252245/738e3ed7-378c-e616-6fe7-82d09685337e.png">

#### Core Graphicで描画
contextを直接操作する方法です。contextを取得する必要があります。
上記１２のdraw()を使うときは引数のcontextを使用しますが、３のdraw()を使うときは、UIGraphicsGetCurrentContext()を使って、CurrentContextを取得する必要があります。

```1のdraw().swift
override func draw(in ctx: CGContext) {
    ctx.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0) )// pathの追加
    ctx.setFillColor(UIColor.blue.cgColor)// 塗りつぶし色の設定
    ctx.fillPath()// パス内の塗りつぶし
}
```

```3のdraw().swift
override func draw(_ rect: CGRect) {
    guard let ctx = UIGraphicsGetCurrentContext() else { assert(false) }
    ctx.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0) )
    ctx.setFillColor(UIColor.blue.cgColor)
    ctx.fillPath()
}
```

#### UIKitで描画
CurrentContextに描画する方法です。contextを取得する必要はありません。
上記１２のdraw()を使うときは、引数のcontextをUIGraphicsPushContext()によってCurrentContextに設定する必要があります。
３のdraw()を使うときは、自動的に対象contextがCurrentContextに設定されているので、何かする必要はありません。
layerサブクラスの`draw(_ rect: CGRect)`を使用する場合は、UIGraphicsGetCurrentContext()でcurrent contextを取得します。
delegateの`draw(_ layer: CALayer, in ctx: CGContext)`を使用する場合は、UIGraphicsPushContext()によって、引数のcontextをCurrentContextに設定する必要があります。


```1のdraw().swift
override func draw(in ctx: CGContext) {
    UIGraphicsPushContext(ctx)
    let p = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
    UIColor.blue.setFill()
    p.fill()
}
```

```3のdraw().swift
override func draw(_ rect: CGRect) {
    let p = UIBezierPath(ovalIn: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))     
    UIColor.blue.setFill()
    p.fill()
}
```

### Layerテンプレート
CALayerを継承したテンプレートLayerが用意されており、これを使うと、
わざわざdraw()を設定するためのクラスを作成しなくても、layerのcontextを設定することが可能です。

テンプレートごとに用途は限定されるのですが、context自己設定機能は便利なので使う時も多いかと思います。

<b>CAShapeLayer</b>
`path`プロパティを持ち、pathの描画を登録できます。
一番使う頻度が高いカスタムlayerかもしれません。
<b>CATextLayer</b>
文字列の描画を登録できます。
<b>CAGradientLayer</b>
グラデーションの描画を登録できます。

例えば、CAShapeLayerで青丸を描画するコードは以下です。

```sample.swift
override func viewWillAppear(_ animated: Bool) {
    super .viewWillAppear(animated)
        
    let sl = CAShapeLayer()
    sl.path = CGPath(
        ellipseIn: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0),
        transform: nil)
    sl.fillColor = UIColor.blue.cgColor
        
    sl.frame = view.layer.bounds
    view.layer.addSublayer(sl)
    sl.setNeedsDisplay()// sublayerの描画
}
```

## その他
### clipToBoundと影の併用
layerの`clipToBounds`パラメータは、bounds外への描画を許可するかどうかを決めます。
layerの影は`shadowColor`,`shadowOffset`,`shadowRadius`,`shadowOpacity`プロパティによって設定でき、alphaが0でないpixelに対して影の描画がそのlayerのcontextに追加されます。
そしてこの影は、bounds外にはみ出ることがあります。

`clipToBounds`がtrueになっていると、はみ出た影は描画されません。

解決方法は、影をつけたいlayerの`bounds`と同じ大きさの親layerを作り、sublayerとして持たせることです。親layerに対して影をつけることで、clipToBoundと影の併用が実現できます。

### 描画しない方法 clear/clip/mask　※保留


# 3.Animation ※保留

# 4.Touch
この章では、UIViewの機能の中で、描画の次にたくさん触れるであろうタッチ機能を説明します。

## タップ情報取得までの流れ
ざっくりいうと、以下の流れでタップ情報がViewまで届きます。

デバイスがタップ情報を検知
↓
applicationに渡される
↓
該当のViewに渡される

以下もう少し詳細を説明します。

### UITouchとUIEvent
![IMG_6952.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252245/b1d5ec3e-6b15-83fc-194e-8903daad204a.jpeg)

##### UITouch
指一つあたりに一つインスタンスが生成されます。
その指が離れるまで同じインスタンスが保持され、離れるとデリートされます。
タップに関する情報を持っています。（タップ座標、開始時のview、今タップ中のviewなど）
##### UIEvent
全てのUITouchインスタンスを保持します。
UITouchの数が0->1になった時にインスタンスが生成され、1->0になった時にインスタンスがデリートします。

#### UITouchの状態
UITouchは、状態を格納する`phase`パラメータを持ちます。
主に以下の値を取ります。
`.began`:　タッチ開始
`.moved`:　タッチが移動
`.stationary`:　タッチがその場にとどまる。（変化なし）
`.ended`:　タッチ終了
`.canceled`: タッチ中断

https://developer.apple.com/documentation/uikit/uitouch/phase

### UIViewがタップ情報を受け取る
UIEventを受け取って処理するためのUIResponderというクラスが存在します。
最初の写真からもわかりますが、UIViewはUIResponderを継承しています。

どれか一つでもUITouchインスタンスのphaseの値が変化すると、UIEventインスタンスが該当のview（phaseが変化したUITouch座標のview）に送られます。

UIResponderは、UITouchのphaseに対応した検知メソッドを持ちます。変化したphaseに対応する検知メソッドが一つだけ呼ばれます。
`touchesBegan(Set<UITouch>, with: UIEvent?)`
`touchesMoved(Set<UITouch>, with: UIEvent?)`
`touchesEnded(Set<UITouch>, with: UIEvent?)`
`touchesCancelled(Set<UITouch>, with: UIEvent?)`

https://developer.apple.com/documentation/uikit/uiresponder


## タップ検知
### 方法１：生のタップ情報を使う（UIResponder）
UIResponderの上記検知メソッドを使用します。
自由度が高く、複雑なタップ検知を設定可能ですが、その反面簡単なタップ検知も設定が難しくなりがちです。
可能なら、UIGestureRecognizerのテンプレートを使った方が楽で良いかと思います。

３タップ後にドラッグして再度タップ、などの特殊なタップ検知などで必要になります。

以前簡単な例を記事を書きました。

https://qiita.com/Zousaaaaaan/items/7e07804fe70b0647d499

### 方法２：UIGestureRecognizerを使う。
実は、viewが受け取ったタップ情報は、viewのもつUIGestureRecognizerインスタンスにも送られます。
Viewは`gestureRecognizers`パラメータにインスタンスを格納しています。

UIGestureRecognizerはUIResponderではないのですが、UIResponderの検知メソッドとほぼ同じメソッドを持ち、UIRecognizerと同様の働きをします。(どうしてこの二つを別々のクラスにしたのか不思議ですが、Androidも同様な仕組みが存在した気がします。)
https://developer.apple.com/documentation/uikit/uigesturerecognizer

##### テンプレート
UIGestureRecognizerは豊富なテンプレートを持っています。

https://developer.apple.com/documentation/uikit/uigesturerecognizer

ワンタップ、ドラッグ、ピンチ、回転、ダブルタップ、、、
などなど典型的なタップ検知は大概用意してくれています。大概この範囲で済みそうですね。

こちらも以前簡単な例を記事を書きました。

https://qiita.com/Zousaaaaaan/items/70c7f701aaa8f2c8c09c

# 参考文献
https://www.amazon.com/Programming-iOS-13-Controllers-Frameworks/dp/1492074616

https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_overview/dq_overview.html#//apple_ref/doc/uid/TP30001066-CH202-TPXREF101