【iOSアプリ開発】Viewの座標やサイズを取得するノウハウ

iOSアプリ開発で、Viewを操作するコードを書く際に、色んな用語が出てきます。
frame, bounds, width, height, origin, center, x, y……
僕は長らく雰囲気で使っていましたが、やはり雰囲気ではマズいだろうということで、
Viewの座標やサイズを取得するノウハウについてまとめたいと思います。

# そもそもiOSアプリの座標系
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333454/9ed1d4d3-abaf-3a2f-99a0-ab11305e14ea.png)
他の画面開発がどうしてるのか知りませんが、iOSアプリは左上を原点（0, 0）とします。
x軸については右に行くほどプラス、y軸については下に行くほどプラスです。

# frameとboundsの違い
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333454/d3427548-a7b1-3a28-6b95-b6050353bb72.png)
frame: superviewの座標系での位置と大きさを示す
bounds: 自己の座標系での位置と大きさを示す
ともにCGRect型

# Core Graphicsについて
CGなんとか型、というのがたくさんでてきますが、これはCore Graphicsというライブラリのクラスです。
iOS端末の画面を描画するためのライブラリですが、詳しく知りたければ下記を見てください。

https://developer.apple.com/documentation/coregraphics

# CGRect
Rectangle（長方形）を描くクラス

```swift:例
let rect = CGRect(x: 0, y: 0, width: 100, height: 100
```

# CGPoint
座標系の任意の地点

```swift:例
let point = CGPoint(x: 50, y: 50)
```

# CGFloat
小数点を表すクラス。
Viewまわりの処理をしていると、この型で指定しないとダメなときがたまにある。

## 普通のFloatと何が違うの？
32ビットCPUだとFloatとして振る舞い、64ビットCPUだとDoubleとして振る舞う、という特徴を持っています。

[本当は怖いCGFloat](https://qiita.com/keroxp/items/b9f2c0f22ccf3415f6cc)

# [origin](https://developer.apple.com/documentation/coregraphics/cgrect/1454354-origin)
CGRect型オブジェクトの原点を示すプロパティ。
その長方形の左上の座標を返す。

注意点として、ルートView以外にAddSubviewで追加している際は、
自分の親Viewを座標（0, 0）としてoriginを出すため、画面上の座標系と異なる場合がある。

返す値はCGPoint型なので、(x, y)という形式で返ってくる。
origin.x/yとすると、どちらかの値だけとれる。

# [size](https://developer.apple.com/documentation/coregraphics/cgrect/1455155-size)
CGRect型オブジェクトのサイズを示すプロパティ。
(width, height)という形式で返す。
size.width/heightとすると、どちらかの値だけとれる。

# 端末のサイズ
iPhone5Sくらいまでは平和でしたが、6以降の画面サイズの変更っぷりは、
開発者という立場になってみるとエグいですね。

[iPhone画面サイズ早見表（図付き）](https://qiita.com/TD3P/items/b3b754bde584abc98f44)
こちらを見ましょう。
ソースコードで指定する単位は「ポイント」です。

# ポイントとピクセルとインチ
端末をあらわす単位がたくさんあって混乱しますが、整理しましょう。

ポイント: 論理的な描画単位
ピクセル: 画面に出力する際の単位
インチ: 画面の斜め部分の長さを示す。
　　　　一言で画面のサイズ感を伝えられるので、スペック紹介の際はよく使われる。

開発の際に考える単位はポイントだけでいいです。

> ピクセルとは、画像編集アプリケーションにおいて、デバイス画面のサイズや作成するアイコンのサイズを検討する場合に使用する測定単位です。ポイントとは、画面上の描画領域のサイズを検討する際に使用する測定単位です。
　標準解像度のデバイスの画面では、1ポイントが1ピクセルに対応しますが、それ以外の解像度では対応の割合が異なる場合があります。たとえば、Retinaディスプレイでは1ポイントは2ピクセルに相当します。
http://miblog.guruguruheadslab.com/archives/57

# 色々な座標・サイズを取得するコード
ここまでが基礎知識です。
ここからは実際にサイズ・座標を取得する便利コードを書いていきます。

## ルートビューの座標・サイズ
コメントの数字は、iPhoneSEのサイズでデバッグした場合です。

```swift:ルートビュー  
self.view.frame.minY //0
self.view.frame.midY //284.0
self.view.frame.maxY //568.0
self.view.frame.minX //0
self.view.frame.midX //160
self.view.frame.maxX //320
```

## tabBar/navigationBarの座標・サイズ
```swift:ルートビュー
//tabBar/navigationBarがある場合
self.tabBarController?.tabBar.frame.height //49pt
self.navigationController?.navigationBar.frame.height //44pt

//tabBar/navigationBarがないが、値が欲しいとき
//（そんなときあるかわからないが、最初上を知らなくて、これで強引にやったため）
UITabBarController.init().tabBar.frame.height //49pt
UINavigationController.init().navigationBar.frame.height //44pt

//tabBar/navigationBarの上辺が欲しいとき
self.tabBarController?.tabBar.frame.origin.y //519pt
self.navigationController?.navigationBar.frame.origin.y //20pt

```

##SafeAreaの取得
iOS11以上（端末だとiPhone8以降は必ずiOS11以降）は、SafeAreaという概念があります。
そのサイズも取得できます。

```swift:safeArea
if #available(iOS 11.0, *) {
            self.view.safeAreaInsets.top //44pt
            self.view.safeAreaInsets.right //0
            self.view.safeAreaInsets.left //0
            self.view.safeAreaInsets.bottom //34pt
        } 
```

iOS11未満は無理なので固定値しかない……と思っていましたが、実はStatusBarの高さとしてとれそうです。

```swift:StatusBar
UIApplication.shared.statusBarFrame.size.height //20pt
```

##端末の画面サイズ
```swift
UIScreen.mainScreen().bounds
```

# 「bounds.originは通常(0,0)を示す」？
「frameとboundsの違い」で書いたとおり、boundsは四角形の境界を示すオブジェクトなので、本来座標情報は持っていません。
したがって、bounds.originを取得すると、（0,0）が返ってきます……と説明されています。

が、僕は今日boundsのy値がマイナスになっているケースに遭遇して、辛い目にあいました。
UITableViewのbounds.originを取得した際に、それは起こりました。
なんと(0, -64)が返ってきたのです！
64ptという数字は調べるとすぐに納得できました。
僕がデバッグしていた端末はiPhone5だったので、
SafeArea(あるいはstatusBar)が20pt、NavigationBarが44ptありました。
おそらくこの64pt分原点がズラされているようでした。

ここからは推測論になるんですが、
UITableViewはUIScrollViewを継承していて、座標系を色々上手いこと調整してくれるクラスです。
おそらくはSafeArea + NavigationBar分の64pt分のズレによって、下記のどちらかの影響が出ているのだと思われます

1. ルートViewが64pt下になったため、相対的に座標がマイナス扱いになった
2. ルートViewは変化ナシだが、それだとSafeArea + NavigationBarの下にUITableViewがカブってしまうので、調整するために-64pt指定した

デバッグしてみると、画面をスクロールすると、UITableView.bounds.origin.yの値も変化していきました。
UIScrollView特有の現象だとは思うんですが、ちょっと扱いづらかったです。

# 端末のウィンドウの座標系の位置をとれないか（2019/08/21追記）
孫ビューの座標を、ルートビューの座標系で再取得するのは、convert()を使えばいけます。

https://developer.apple.com/documentation/uikit/uiview/1622498-convert

色々触ってみましたが、to/fromをnilにすると、UIWindowの座標系を使ってくれるとのことなので、これでできますね。
ただ、孫ビューよりネストが深くなっていると、何度か噛ませないと上手く変換できないと思われます。

Objective-C時代はconvertRectという名前だったので、そちらの方が参考情報いっぱい出てくるかもです。

このへんとかよかったです
[もう迷わない convertRect](http://www.minimalab.com/blog/2015/10/31/convert-rect/)

# 最後に
誤字脱字、内容の誤りあればご指摘お願いします！！！！！