今更だけどUIViewの座標変換系メソッドを理解する（Swift）

![Xcode-12](https://img.shields.io/badge/Xcode-12-brightgreen) ![iOS-14.0](https://img.shields.io/badge/iOS-14.0-brightgreen) ![Swift-5.3](https://img.shields.io/badge/Swift-5.3-brightgreen)

## はじめに
ごくまれに `UIView` の convert 系のメソッドを使って座標を変換したいときがありますがいつもどっちに何指定するのかな？と忘れるのでまとめておきます。

## View 準備
下記のような構成で座標変換をしてみます。

<img width="242" alt="xib" src="https://user-images.githubusercontent.com/34936885/105171026-1b207780-5b61-11eb-92f3-0d2c7a332782.png">

<img width="150" alt="views" src="https://user-images.githubusercontent.com/34936885/105171042-207dc200-5b61-11eb-8da2-f5a037d76c52.png">

ScrollView を ViewController の View と同じサイズで配置してその中に `contentView` （緑色）を幅は ScrollView と同じ高さは 1200 pt で配置しています（StackView は左端のメモリ用です）。`contentView` の中に縦横 120 pt の `targetView` （白色）を `contentView` の Top 900pt、横は中央寄せで配置して、その `targetView` （白色）の中心にボタンを配置しています。

## 座標変換
座標変換には `UIView` に下記のメソッドが用意されており、これを使って変換します。

* [convert(_: to:)](https://developer.apple.com/documentation/uikit/uiview/1622442-convert)
* [convert(_: from:)](https://developer.apple.com/documentation/uikit/uiview/1622424-convert)

どちらのメソッドも対象の View は同一 Window にある必要があります。引数の `view` が `nil` の場合は Window の座標系に変換されます。
### convert(_: to:)
`point` はレシーバーの座標系を指定し、`view` には変換したい座標系の View を指定します。

```swift
func convert(_ point: CGPoint, to view: UIView?) -> CGPoint

// ex.
aView.convert(CGPoint.zero, to: bView)
```

例だと `aView` の左上の座標を `bView` の座標系に変換しています。

### convert(_: from:)
`point` は `view` の座標系を指定し、レシーバーの座標系に変換します。

```swift
func convert(_ point: CGPoint, from view: UIView?) -> CGPoint

// ex.
aView.convert(CGPoint.zero, from: bView)
```

例だと `bView` の左上の座標を `aView` の座標系に変換しています。

## 実践
ボタン押下時に `targetView` の座標系から `scrollView` と ViewController の `view` の座標系に変換してみます！

```swift
 @IBAction private func convertPoints(_ sender: Any) {
    // targetViewの左上の座標をscrollViewの座標系に変換
    let p1 = targetView.convert(CGPoint.zero, to: scrollView)
    print("p1: \(p1)") // p1: (127.5, 900.0)

    // targetViewの左上の座標をviewの座標系に変換
    let p2 = targetView.convert(CGPoint.zero, to: view)
    print("p2: \(p2)") // p2: (127.5, 367.0) * scrollViewのoffsetで変わる

    // targetViewの左上の座標をscrollViewの座標系に変換 
    let p3 = scrollView.convert(CGPoint.zero, from: targetView)
    print("p3: \(p3)") // p3: (127.5, 900.0)

    // targetViewの左上の座標をviewの座標系に変換 
    let p4 = view.convert(CGPoint.zero, from: targetView)
    print("p4: \(p4)") // p4: (127.5, 367.0) * scrollViewのoffsetで変わる
}
```

上記を見てもらうと分かる通り、p1 と p3、p2 と p4 は同じ結果になります。

これを使ってボタン押下時に `targetView` が画面外にある場合にスクロールのオフセットを調節してみます。

```swift
@IBAction private func convertPoints(_ sender: Any) {
    // targetViewの左下の座標をviewの座標系に変換
    let p1 = targetView.convert(CGPoint(x: 0, y: targetView.frame.height), to: view)
    // targetViewの左下がviewの範囲内か確認
    if !view.frame.contains(p1) {
        // targetViewが画面内におさまるようスクロール
        let p2 = targetView.convert(CGPoint(x: 0, y: targetView.frame.height), to: scrollView)
        scrollView.setContentOffset(.init(x: 0, y: p2.y - scrollView.frame.height), animated: true)
    }
}
```

こんな感じです:tada:

![scroll](https://user-images.githubusercontent.com/34936885/105171082-2bd0ed80-5b61-11eb-87a4-6e15b43078f3.gif)

## おわりに
to と from の意味を考えたらわかるんですが `point` はどの座標系のやついれればいいんだっけ？とよく迷います。。。

もうこれで迷わない！！（たぶん:rolling_eyes:）

ちなみに Rect の変換メソッドもあります。

* [convert(_: to:)](https://developer.apple.com/documentation/uikit/uiview/1622504-convert)
* [convert(_: from:)](https://developer.apple.com/documentation/uikit/uiview/1622498-convert)

SwiftUI になるとこういうの使うこともなくなるのかな:thinking:

## 参考
* [convert(_: from:)](https://developer.apple.com/documentation/uikit/uiview/1622424-convert)
* [convert(_: to:)](https://developer.apple.com/documentation/uikit/uiview/1622442-convert)