キーボード表示でViewをずらすか判定するViewController作ってみた

#はじめに
キーボードの表示非表示処理って簡単なのですが、テキストボックスとかぶったり、viewをずらしたらテキストフィールドが上に行き過ぎて見えなくなったりして面倒だなぁと思ってました。
何とかならないかと試行錯誤したので記事書いてみました。

#考え方
- できるだけ簡単に
- テキストフィールドが増えても何もしなくていい
- テキストフィールドが隠れる時だけずらしたい

以上を踏まえて考えたところ、継承したUIViewControllerを作るのが手っ取り早いかな？と結論に至りました。

#実装

まずはおきまりのキーボードが表示された時と隠れた時、バックグラウンドから復帰した時の通知を拾います。

```swift
class ForInputViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShowNotification(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHideNotification(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.viewWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    @objc override func keyboardWillShowNotification(notification: NSNotification) {
        guard let userInfo = notification.userInfo,
            let keyboard = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
                return
        }
        let keyboardScreenEndFrame = keyboard.cgRectValue
        let myBoundSize: CGSize = UIScreen.main.bounds.size
        // TODO:ここでテキストフィールドが隠れるかどうか判定したい
    }

    @objc override func keyboardWillHideNotification(notification: NSNotification) {

    }

    @objc override func viewWillEnterForeground(notification: Notification) {

    }

}
```

問題はキーボードが表示された時のテキストフィールドの情報がないので、キーボードに隠れるかどうかはわかりません。
そこで、
textFieldShouldBeginEditingイベントで対象のテキストフィールドのインスタンスを取得して変数に保持しておきます。

```swift
    private var txtActiveField = UITextField()
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        txtActiveField = textField
        return true
    }
```
こうすることで、キーボードが表示された時のテキストボックスの座標・高さが取得できるようになります。
そして、キーボードが表示された時に実行される関数（ここではkeyboardWillShowNotification）の中でキーボードでテキストフィールドが隠れるかどうかを判定してあげます。
ポイントはconvertで親のviewからみた座標に変換してあげることですね。

```swift
@objc override func keyboardWillShowNotification(notification: NSNotification) {
        guard let userInfo = notification.userInfo,
            let keyboard = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
                return
        }
        let keyboardScreenEndFrame = keyboard.cgRectValue
        let myBoundSize: CGSize = UIScreen.main.bounds.size

        // textFieldの座標を全体座標に変換
        let textframeParent = txtActiveField.convert(txtActiveField.frame, to: self.view)
        let txtLimit = textframeParent.origin.y + textframeParent.height + 8.0
        let kbdLimit = myBoundSize.height - keyboardScreenEndFrame.size.height

        log.debug("テキストフィールドの下辺：(\(txtLimit))")
        log.debug("キーボードの上辺：(\(kbdLimit))")

        // テキストフィールドがキーボードに隠れる時のみスライドさせる
        if txtLimit >= kbdLimit {
            UIView.animate(withDuration: 0, animations: { () in
                let transform = CGAffineTransform(translationX: 0, y: -(keyboardScreenEndFrame.size.height))
                self.view.transform = transform
            })
        }
    }

```
そしてキーボードが隠れる時やバックグラウンドからの復帰処理も入れます。

```swift
    @objc override func keyboardWillHideNotification(notification: NSNotification) {
        UIView.animate(withDuration: 0, animations: { () in
            self.view.transform = CGAffineTransform.identity
        })
    }

    @objc override func viewWillEnterForeground(notification: Notification) {
        UIView.animate(withDuration: 0, animations: { () in
            self.view.transform = CGAffineTransform.identity
        })
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
```

後はこのできたViewControllerを継承すればいいのですが、対象のUITextFieldのdelegateを設定してあげる必要があります。普通なら使うcontroller側で以下のようにすると思いますが、

```swift
    @IBOutlet private weak var textField: UITextField! {
        didSet {
            textField.delegate = self
        }
    }
```
これだとテキストフィールドが増えると面倒です。

そこで、自分が保持しているtextField全てにdelegateを自動で設定するようにviewDidLoadに組み込みます。

```swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // 全てのテキストフィールドのdelegate設定
        let textFileds = getAllTextFields(fromView: self.view)
        for textFiled in textFileds {
            textFiled.delegate = self
        }
    }

    private func getAllTextFields(fromView view: UIView) -> [UITextField] {
        return view.subviews.compactMap { (view) -> [UITextField]? in
            return (view is UITextField) ? [(view as! UITextField)] : getAllTextFields(fromView: view)
        }.flatMap({$0})
    }
```
これでTextFieldがいくら増えても自動で隠れるかどうか判定してキーボードを表示してくれます。

#完成ソース

```swift
class ForInputViewController: UIViewController, UITextFieldDelegate {

    private var txtActiveField = UITextField()

    // MARK: - Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        // 全てのテキストフィールドのdelegate設定
        let textFileds = getAllTextFields(fromView: self.view)
        for textFiled in textFileds {
            textFiled.delegate = self
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShowNotification(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHideNotification(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.viewWillEnterForeground(notification:)), name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }

    @objc func keyboardWillShowNotification(notification: NSNotification) {
        guard let userInfo = notification.userInfo,
            let keyboard = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
                return
        }
        let keyboardScreenEndFrame = keyboard.cgRectValue
        let myBoundSize: CGSize = UIScreen.main.bounds.size

        // textFieldの座標を全体座標に変換
        let textframeParent = txtActiveField.convert(txtActiveField.frame, to: self.view)
        let txtLimit = textframeParent.origin.y + textframeParent.height + 8.0
        let kbdLimit = myBoundSize.height - keyboardScreenEndFrame.size.height

        log.debug("テキストフィールドの下辺：(\(txtLimit))")
        log.debug("キーボードの上辺：(\(kbdLimit))")

        // テキストフィールドがキーボードに隠れる時のみスライドさせる
        if txtLimit >= kbdLimit {
            UIView.animate(withDuration: 0, animations: { () in
                let transform = CGAffineTransform(translationX: 0, y: -(keyboardScreenEndFrame.size.height))
                self.view.transform = transform
            })
        }
    }

    @objc func keyboardWillHideNotification(notification: NSNotification) {
        UIView.animate(withDuration: 0, animations: { () in
            self.view.transform = CGAffineTransform.identity
        })
    }

    @objc func viewWillEnterForeground(notification: Notification) {
        UIView.animate(withDuration: 0, animations: { () in
            self.view.transform = CGAffineTransform.identity
        })
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.view.endEditing(true)
    }

    // MARK: - public

    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        txtActiveField = textField
        return true
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }

    // MARK: - private

    private func getAllTextFields(fromView view: UIView) -> [UITextField] {
        return view.subviews.compactMap { (view) -> [UITextField]? in
            return (view is UITextField) ? [(view as! UITextField)] : getAllTextFields(fromView: view)
        }.flatMap({$0})
    }

}

```
使い方は継承するだけなのでいい感じに手抜きできると思いますwww
効率よい仕事につながりますね！

#参考
大変勉強になりました。ありがとうございましたm(_ _)m
- [[Swift] UITextFieldがキーボードに隠れないようにするやり方](https://qiita.com/kobaboy/items/d56086b92f84c586562d)


#更新（11/25）
上記のようにviewDidLoadでdelegateをセットするとテーブルビューなどコードでセットするテキストフィールドには反応しません。そこでdelegateのセットのタイミングをviewDidAppearに変更することで対応ができました。

```swift
    private var initialized: Bool = false

    override func viewDidLoad() {
        super.viewDidLoad()
        initialized = false
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        guard !initialized else { return }

        // 全てのテキストフィールドのdelegate設定
        setDelegate()
    }

    private func setDelegate() {
        let textFileds = getAllTextFields(fromView: self.view)
        for textFiled in textFileds {
            textFiled.delegate = self
        }
        initialized = true
    }
```

また、UIScrollViewを使用している場合はtouchesBeganイベントを伝搬させてあげる必要があります。

```swift
extension UIScrollView {
    override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.next?.touchesBegan(touches, with: event)
    }
}
```

UITextViewも同じ処理で実現できます！