UITextFieldをタップした時に画面をスクロールする

テキスト入力欄をタップした時に、テキストボックスが隠れないように画面をスクロールする方法。 これはUITextFieldを使う上で必須だと思うためメモ。

目次

何も設定しなかった場合の例

TextFieldを画面の下の方に設置するだけ。 そうすると、UITextFieldをタップした時に以下のようになります。 実行結果
タップ前 タップ後

UITextFieldのデリゲートメソッドを実装する

今回の目的は「UITextFieldをタップした時に画面をスクロールする」でしたね。 なので、UITextFieldタップされた時に呼び出されるメソッドを実装しましょう。 詳しくはUITextFieldのデリゲートメソッドの項目を参照してください。 それを実装したコードはこんな感じです。
import UIKit

class FirstViewController : UIViewController, UITextFieldDelegate {
    @IBOutlet var textField : UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.textField.delegate = self
    }
    
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        print("テキストフィールドがタップされた")
        return true
    }
}
実行すると、textFieldShouldBeginEditingメソッドに書いた処理が行われます。

キーボードが出現した時に処理を実行するメソッドを作る

次はキーボードが表示されたら呼び出されるメソッドを作ります。 キーボードはUITextFieldをタップすれば勝手に出てきますから… ここで使われるのがNSNotificationとkeyboardWillShowメソッドなどです。 NSNotificationに関してはこちらを参照してください。 要は、以下のように書いておくと、キーボードが表示されたときにkeyboardWillShowメソッドが呼ばれ、 キーボードが非表示になったときにkeyboardWillHideメソッドが呼ばれると言う感じです。
let notification = NotificationCenter.default
notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
import UIKit

class FirstViewController : UIViewController, UITextFieldDelegate {
    @IBOutlet var textField : UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // デリゲートを設定
        self.textField.delegate = self
        
        let notification = NotificationCenter.default
        notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }
    
    // キーボードが現れたときに実行
    @objc func keyboardWillShow(notification: Notification?) {
        print("キーボードが表示された")
    }
    
    // キーボードが消えたときに実行
    @objc func keyboardWillHide(notification: Notification?) {
        print("キーボードが消された")
    }
    
    func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
        print("テキストフィールドがタップされた")
        return true
    }
}

全体をスクロールする

さて、ここからが本題です。 UITextFieldをタップしてキーボードが表示されたら画面全体をスクロールしてあげます。 実装する手段は複数ありますが、今回はCGAffineTransformを使うことにします。 CGAffineTransformの詳細はこちらを参照してください。 以下の処理でどれくらい画面全体を移動させるかを引数に設定します。 今回、UITextFieldをタップした際、キーボードのすぐ上に位置するようにUITextFieldの下マージンをキーボードの高さから引いてあげます。
let affine = CGAffineTransform.init(translationX: 0.0, y: -self.scrollByKeyboard)
// キーボードの大きさを取得
let keyboardFrame : CGRect = (notification?.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// キーボードのすぐ上にテキストフィールドが来るように調整する
self.scrollByKeyboard = keyboardFrame.size.height - (self.view.frame.height - self.textField.frame.maxY)
ここで注意なのは、引数に設定する座標の値は、画面の左上の座標を基準としていることです。 なので、キーボードが隠れて元に戻る場合は以下のように設定してあげます。
let affine = CGAffineTransform.init(translationX: 0.0, y: 0.0)
僕は始め、今の位置からどれくらい動かすかを引数に設定すると思っていました(^^; そして、移動させる設定をした後は、UIViewのクラスメソッドのanimateメソッドで、指定した時間で徐々に移動させていくと言う処理を実現させます。 以下の例だと、「0.3秒かけて指定した位置へ移動させる」という処理をしています。 animateメソッドに関してはこちらを参照してください。 クラスメソッドについてはこちら
UIView.animate(withDuration: 0.3,
                 animations: {
                     self.view.transform = affine
                 },
                 completion: nil)
そして、それらを合わせた全体のコードが以下の通りになります。
import UIKit

class FirstViewController : UIViewController, UITextFieldDelegate {
    @IBOutlet var textField : UITextField!
    
    // キーボード出現によるスクロール量
    var scrollByKeyboard : CGFloat = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // デリゲートを設定
        self.textField.delegate = self
        
        let notification = NotificationCenter.default
        notification.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        notification.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }
    
    // キーボードが現れたときに実行
    @objc func keyboardWillShow(notification: Notification?) {
        print("キーボードが表示された")
        
        // キーボードの大きさを取得
        let keyboardFrame : CGRect = (notification?.userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
        // キーボードのすぐ上にテキストフィールドが来るように調整する
        self.scrollByKeyboard = keyboardFrame.size.height - (self.view.frame.height - self.textField.frame.maxY)
        
        // 画面をスクロールさせる
        let affine = CGAffineTransform.init(translationX: 0.0, y: -self.scrollByKeyboard)
        // 画面のスクロールをアニメーションさせる
        UIView.animate(withDuration: 0.3,
                       animations: {
                        self.view.transform = affine
        },
                       completion: nil)
    }
    
    // キーボードが消えたときに実行
    @objc func keyboardWillHide(notification: Notification?) {
        print("キーボードが消された")
        
        // 画面のスクロールを元に戻す
        let affine = CGAffineTransform.init(translationX: 0.0, y: 0.0)
        // 画面のスクロールをアニメーションさせる
        UIView.animate(withDuration: 0.3,
                       animations: {
                        self.view.transform = affine
        },
                       completion: { (true) in
                        self.scrollByKeyboard = 0.0
        })
    }
    
    // キーボードを編集するときに呼ばれるデリゲートメソッド
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        print("テキストフィールドがタップされた")
        
        return true
    }
    
    // キーボードのReturnキーを押したときに呼ばれるデリゲートメソッド
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // キーボードを閉じる
        textField.resignFirstResponder()
        
        print("リターンキーが押された")
        return true
    }
}
実行結果
タップ前 タップ後