UIScrollVIewの上でも、キーボードが現れたときにいい感じにスクロールする

備忘録兼紹介です。
UIScrollView × 複数のUITextFieldで、キーボードが出たときにスクロールさせる処理を紹介します。

## 動き
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208118/ec432b9a-05e2-9750-c7e9-b1550967f0f2.gif" width="300">

## コード
```Swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var scroll: UIScrollView!    // UITextFieldとかを配置するView
    @IBOutlet weak var stack: UIStackView!      // 表示サンプル用
    
    // ScrollViewのBottomの制約
    @IBOutlet weak var scrollViewBottomConstraints: NSLayoutConstraint!
    
    private var nowEditingField:UITextField!    // 編集中のUITextField
    private var offsetAdded:CGFloat?            // スクロール戻す時用
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 表示サンプルとして、UILabelを20個配置
        let lines = 20
        for count in 1...lines {
            let field = UITextField()
            field.placeholder = "\(count)"
            field.borderStyle = .roundedRect
            field.textAlignment = .center
            
            field.delegate = self // ← UITextFieldのdelegateを拾いたいので、delegateを設定する
            
            self.stack.addArrangedSubview(field)
            field.widthAnchor.constraint(equalToConstant: 250.0).isActive = true
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // キーボードが表示されるときと消えるときに動作させるメソッドを登録
        // キーボードの高さがほしかった
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        NotificationCenter.default.removeObserver(UIResponder.keyboardWillShowNotification)
    }
}

extension ViewController: UITextFieldDelegate {
    
    // テキストフィールドにフォーカスがあたった時に呼ばれる
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        self.nowEditingField = textField
        return true
    }
    
    // テキストフィールドからフォーカスが外れる時に呼ばれる
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        self.nowEditingField = nil
        let offsetAdded = self.offsetAdded
        self.offsetAdded = nil
        var offset:CGPoint = CGPoint(x: 0, y: 0)
        if self.scroll.contentOffset.y - (offsetAdded ?? 0) < 0 {
            // 上端
            offset = CGPoint(x: self.scroll.contentOffset.x, y: 0)
        }else if self.scroll.contentOffset.y > (self.scroll.contentSize.height - self.scroll.frame.size.height - self.scrollViewBottomConstraints.constant) {
            // 下端
            offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y)
        }else {
            // その他
            offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y - (offsetAdded ?? 0))
        }

        self.scrollViewBottomConstraints.constant = 0
        self.scroll.setContentOffset(offset, animated: true)
        
        return true
    }

    // キーボードが表示される直前に呼ばれる　※textFieldShouldBeginEditingより後に呼ばれる
    @objc func keyboardWillShow(_ notification: Notification) {
        guard let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
            return
        }
        guard let field = self.nowEditingField else { return }

        let keyboardHeight = keyboardFrame.cgRectValue.height
        let keyboardTopLine = self.view.frame.height - keyboardHeight

        let bottomLine = field.frame.origin.y + field.frame.height
        let displayBottom = bottomLine - self.scroll.contentOffset.y

        if displayBottom > keyboardTopLine {
            let sub = displayBottom - keyboardTopLine
            self.offsetAdded = sub
            let offset = CGPoint(x: self.scroll.contentOffset.x, y: self.scroll.contentOffset.y + sub)

            self.scroll.setContentOffset(offset, animated: true)
            self.scrollViewBottomConstraints.constant = keyboardHeight
        }
    }
    
    // キーボードのリターンでキーボードを閉じる
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        self.view.endEditing(true)
        return true
    }
}

```
---
ありがとうございました。
よかったら使ってください。(もっといい実装があれば教えて下さい。)