【Swift】UITextFieldがキーボードに隠れる場合のみ画面をずらす（UIScrolleViewなし）

## はじめに
Swiftでは何もしなければ、`UITextField`がキーボードで隠れてしまいます。
それを改善するために、`UIScrollView`を使わずに画面をずらす方法を実装しました。
`UITextField`がキーボードに隠れる場合のみ画面をずらすように実装しています。

これに関する情報や記事は多くあると思うのですが、ずらす幅の調整などで少し時間がかかってしまったので、記事として残しておきます。

コメントなどは、自分の分かりやすいように書いているので、細かい認識などは間違っている場合がありますが、ご容赦下さい。


## １．キーボード開閉のタイミングを取得

まず、`viewWillAppear`に`Notification`を用いてキーボードが開閉するタイミングを取得する処理を記載。

```Swift
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // キーボード開閉のタイミングを取得
        let notification = NotificationCenter.default
        notification.addObserver(self, selector: #selector(self.keyboardWillShow(_:)),
                                 name: UIResponder.keyboardWillShowNotification,
                                 object: nil)
        notification.addObserver(self, selector: #selector(self.keyboardWillHide(_:)),
                                 name: UIResponder.keyboardWillHideNotification,
                                 object: nil)
    }
```

## ２．キーボード表示通知の際の処理(keyboardWillShow)

```Swift
    @objc func keyboardWillShow(_ notification: Notification) {

        // 編集中のtextFieldを取得
        guard let textField = _activeTextField else { return }
        // キーボード、画面全体、textFieldのsizeを取得
        let rect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
        guard let keyboardHeight = rect?.size.height else { return }
        let mainBoundsSize = UIScreen.main.bounds.size
        let textFieldHeight = textField.frame.height

        // ①
        let textFieldPositionY = textField.frame.origin.y + textFieldHeight + 10.0
        // ②
        let keyboardPositionY = mainBoundsSize.height - keyboardHeight

        // ③キーボードをずらす
        if keyboardPositionY <= textFieldPositionY {
            let duration: TimeInterval? =
                notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
            UIView.animate(withDuration: duration!) {
                // viewをy座標方向にtransformする
                self.view.transform = CGAffineTransform(translationX: 0, y: keyboardTopPositionY - textFieldTopPositionY)
            }
        }
    }
```

y座標の0は、上部であることを意識すると分かりやすいと思います。

① `textField.frame.origin.y + textFieldHeight + 10.0`で、テキストフィールドの底辺より10.0下のy座標を`textFieldPositionY`として取得。（テキストフィールドとキーボードの間に隙間ができるように`+10.0`としています。
② `mainBoundsSize.height - keyboardHeight`で、キーボードの底辺のy座標を`keyboardPositionY`として取得。
③ ①が②より大きい（①が②の下にある）時に、viewをずらす。`keyboardTopPositionY - textFieldTopPositionY`で、テキストフィールドが隠れている分だけずらす。
※ viewをキーボードの高さ分ずらしたい時は、`-keyboardHeight`にします。

## ３．キーボード非表示通知の際の処理(keyboardWillHide)

```Swift
    @objc func keyboardWillHide(_ notification: Notification) {
        let duration: TimeInterval? = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
        UIView.animate(withDuration: duration!) {
            self.view.transform = CGAffineTransform.identity
        }
    }
```

## その他、TextFieldに関する処理

```Swift
    // textFieldがタップされた際に呼ばれる
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        // 編集中のtextFieldを保持する
        _activeTextField = textField
        return true
    }
    
    // リターンがタップされた時にキーボードを閉じる
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
    // 画面をタップした時にキーボードを閉じる
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
```

## コード全体
以下、コードの全文です。
```swift:ViewController.swift
import UIKit

class ViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet private var textField: UITextField!

    // 編集中のtextFieldを保持する変数
    private var _activeTextField: UITextField? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        
        textField.delegate = self
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // キーボード開閉のタイミングを取得
        let notification = NotificationCenter.default
        notification.addObserver(self, selector: #selector(self.keyboardWillShow(_:)),
                                 name: UIResponder.keyboardWillShowNotification,
                                 object: nil)
        notification.addObserver(self, selector: #selector(self.keyboardWillHide(_:)),
                                 name: UIResponder.keyboardWillHideNotification,
                                 object: nil)
    }

    // キーボード表示通知の際の処理
    @objc func keyboardWillShow(_ notification: Notification) {
        // 編集中のtextFieldを取得
        guard let textField = _activeTextField else { return }
        // キーボード、画面全体、textFieldのsizeを取得
        let rect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
        guard let keyboardHeight = rect?.size.height else { return }
        let mainBoundsSize = UIScreen.main.bounds.size
        let textFieldHeight = textField.frame.height

        let textFieldPositionY = textField.frame.origin.y + textFieldHeight + 10.0
        let keyboardPositionY = mainBoundsSize.height - keyboardHeight
        
        if keyboardPositionY <= textFieldPositionY {
            let duration: TimeInterval? =
                notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double
            UIView.animate(withDuration: duration!) {
                self.view.transform = CGAffineTransform(translationX: 0, y: keyboardTopPositionY - textFieldTopPositionY)
            }
        }
    }

    // キーボード非表示通知の際の処理
    @objc func keyboardWillHide(_ notification: Notification) {
        let duration: TimeInterval? = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? Double
        UIView.animate(withDuration: duration!) {
            self.view.transform = CGAffineTransform.identity
        }
    }

    // textFieldがタップされた際に呼ばれる
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        // 編集中のtextFieldを保持する
        _activeTextField = textField
        return true
    }
    
    // リターンがタップされた時にキーボードを閉じる
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
    // 画面をタップした時にキーボードを閉じる
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }
}
```

## 参考URL

https://www.peliphilo.net/archives/2352

https://pype.org/swift-get-keyboard-height/