Help us understand the problem. What is going on with this article?

キーボードが表示されたときに UITextField を上にスクロールさせる方法

はじめに

よくある入力フォームで、テキストフィールドをタップしたときには、キーボードに隠れないように、自動的にテキストフィールドが上にスクロールしてほしいですよね。
コーディングしなくても勝手にキーボードを回避してくれる機能はSwiftにはありませんので、自分で実装する必要があります。
世の中いろいろサンプルが出回っていますが、自分なりにようやく書けるようになったので、ここにまとめます。

当記事のコードの特徴

  • テキストフィールドにタッチすると、
    • キーボードに隠れない場合は、何も起きません。
    • キーボードに隠れる場合は、テキストフィールドがキーボードの上辺に合わせてスクロールします。
  • ユーザーが入力を完了すると、キーボードが下がるのに合わせて、テキストフィールドが元の位置にスクロールします。
  • 対象のテキストフィールドがアクティブなときに、キーボードツールバーに上/下ボタンや完了ボタンを設定します。

実装イメージ

keyboardupdownsample.gif

コードの説明

ここで説明するコードは、以下のリポジトリで公開しています。
https://github.com/mnaruse/KeyboardUpDownSample

Storyboard での注意点

UITextFieldを、UIScrollViewの中に設置しておく必要があります。

IBOutlet での注意点

大抵の場合、複数のUITextFieldが置いてあると思いますので、[UITextField] というUITextFieldsの配列を作っておきます。

@IBOutlet private var textFields: [UITextField]!

ViewControllerのファイルと、Storyboardのファイルを並べて開き、このコード上のIBOutletから、Storyboard内の対象のUITextFieldに向かって、接続していきます。
最終的にこんな感じになります。

iboutlet_textfields.png

キーボードのサイズが変化すると実行されるイベントハンドラー のコードのポイント

キーボードのサイズが変化すると実行される関数について、ポイントを説明します。

/// キーボードのサイズが変化すると実行されるイベントハンドラー
/// テキストフィールドが隠れたならスクロールする。
///
/// キーボードの退場でも同じイベントが発生するので、編集中のテキストフィールドがnilの時は処理を中断する。
@objc private func keyboardChangeFrame(_ notification: Notification) {
    // 編集中のテキストフィールドがnilの時は処理を中断する。
    guard let textField = editingTextField else {
        return
    }

    // キーボードのframeを調べる。
    let userInfo = notification.userInfo
    let keyboardFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as!
NSValue).cgRectValue

以下のように、編集中のUITextFieldのframeを、キーボードのframeと同じ座標系にするところがポイントです。
これによって、重なり具合の比較が容易になります。

    // テキストフィールドのframeをキーボードと同じウィンドウの座標系にする。
    guard let textFieldFrame = view.window?.convert(textField.frame, from: textField.superview)
else {
        return
    }

次に、以下のように、編集中のテキストフィールド足す余白分(以下の例では8ptに設定しています)が、キーボードと重なっていないか調べて、それをスクロールするかしないかの判断基準にするところもポイントです。

  • 重なっていなかったら、何もせず処理を終了します。
  • 重なっていたら、テキストフィールドをキーボードの上辺に合わせて上にスクロールします。
    /// テキストフィールドとキーボードの間の余白(自由に変更してください。)
    let spaceBetweenTextFieldAndKeyboard: CGFloat = 8
    // 編集中のテキストフィールドがキーボードと重なっていないか調べる。
    // 重なり = (テキストフィールドの下端 + 余白) - キーボードの上端
    var overlap = (textFieldFrame.maxY + spaceBetweenTextFieldAndKeyboard) - keyboardFrame.minY
    if overlap > 0 {
        // 重なっている場合、キーボードが隠れている分だけスクロールする。
        overlap = overlap + scrollView.contentOffset.y
        scrollView.setContentOffset(CGPoint(x: 0, y: overlap), animated: true)
    }
}

コード全体

コードの全体は以下の通りです。

なお、コード内で実装するかしないか任意な点は以下の通りです。

  • 対象のテキストフィールドがアクティブなとき、キーボードのツールバーに、上下ボタンや完了ボタンを設定するかどうかは任意です。
  • ビューがタップされた時に、キーボードを下げるかどうかは任意です。

コード全体

//
//  ViewController.swift
//  KeyboardUpDownSample
//
//  Created by Miharu Naruse on 2020/11/15.
//

import UIKit

class ViewController: UIViewController {
    @IBOutlet private var scrollView: UIScrollView!
    @IBOutlet private var textFields: [UITextField]!

    /// 編集中のテキストフィールド
    private var editingTextField: UITextField?

    /// キーボードが登場する前のスクロール量
    private var lastOffsetY: CGFloat = 0.0

    override func viewDidLoad() {
        super.viewDidLoad()

        // 全てのテキストフィールドのデリゲートになる。
        for textField in textFields {
            textField.delegate = self
        }
        // テキストフィールドとキーボード関連の処理について、通知センターの設定をする。
        setNotificationCenter()

        // 任意: 対象のテキストフィールドがアクティブなとき、キーボードのツールバーに、前後ボタンや完了ボタンを設定する。
        addPreviousNextableDoneButtonOnKeyboard(textFields: textFields, previousNextable: true)
    }

    /// 任意: ビューがタップされた時に実行される処理
    @IBAction func tapView(_ sender: UITapGestureRecognizer) {
        // キーボードを下げる。
        view.endEditing(true)
    }
}

// MARK: - Extensions テキストフィールドとキーボード関連の処理

extension ViewController {
    /// テキストフィールドとキーボード関連の処理について、通知センターの設定をする。
    private func setNotificationCenter() {
        /// デフォルトの通知センターを取得
        let notification = NotificationCenter.default

        // キーボードのframeが変化した時のイベントハンドラーを登録する。
        notification.addObserver(self, selector: #selector(keyboardChangeFrame(_:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil)

        // キーボードが登場する時のイベントハンドラーを登録する。
        notification.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)

        // キーボードが退場する時のイベントハンドラーを登録する。
        notification.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    /// キーボードのサイズが変化すると実行されるイベントハンドラー
    /// テキストフィールドが隠れたならスクロールする。
    ///
    /// キーボードの退場でも同じイベントが発生するので、編集中のテキストフィールドがnilの時は処理を中断する。
    @objc private func keyboardChangeFrame(_ notification: Notification) {
        // 編集中のテキストフィールドがnilの時は処理を中断する。
        guard let textField = editingTextField else {
            return
        }

        // キーボードのframeを調べる。
        let userInfo = notification.userInfo
        let keyboardFrame = (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue

        // テキストフィールドのframeをキーボードと同じウィンドウの座標系にする。
        guard let textFieldFrame = view.window?.convert(textField.frame, from: textField.superview) else {
            return
        }

        /// テキストフィールドとキーボードの間の余白(自由に変更してください。)
        let spaceBetweenTextFieldAndKeyboard: CGFloat = 8

        // 編集中のテキストフィールドがキーボードと重なっていないか調べる。
        // 重なり = (テキストフィールドの下端 + 余白) - キーボードの上端
        var overlap = (textFieldFrame.maxY + spaceBetweenTextFieldAndKeyboard) - keyboardFrame.minY
        if overlap > 0 {
            // 重なっている場合、キーボードが隠れている分だけスクロールする。
            overlap = overlap + scrollView.contentOffset.y
            scrollView.setContentOffset(CGPoint(x: 0, y: overlap), animated: true)
        }
    }

    /// キーボードが登場する通知を受けると実行されるイベントハンドラー
    @objc private func keyboardWillShow(_ notification: Notification) {
        // キーボードが登場する前のスクロール量を保存しておく。
        lastOffsetY = scrollView.contentOffset.y
    }

    /// キーボードが退場する通知を受けると実行されるイベントハンドラー
    @objc private func keyboardWillHide(_ notification: Notification) {
        // スクロール量をキーボードが登場する前の位置に戻す。
        scrollView.setContentOffset(CGPoint(x: 0, y: lastOffsetY), animated: true)
    }
}

// MARK: - Extensions UITextFieldDelegate テキストフィールドとキーボード関連の処理

extension ViewController: UITextFieldDelegate {
    func textFieldDidBeginEditing(_ textField: UITextField) {
        // テキストフィールドの編集が開始された時に実行される処理。
        // どのテキストフィールドが編集中か保存しておく。
        editingTextField = textField
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        // テキストフィールドの編集が終了した時に実行される処理。
        // 編集中のテキストフィールドをnilにする。
        editingTextField = nil
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // 改行キーが入力された時に実行される処理。
        // キーボードを下げる。
        view.endEditing(true)
        // 改行コードは入力しない。
        return false
    }
}

おわりに

読んでいただきありがとうございました。
何か少しでもお役に立てたら嬉しいです:pray:
「こういうやり方もあるよ」とか「ここはこうした方がいいよ」とか、コメントやアドバイス、あるいは質問があれば、よかったらコメントお願いします。

関連記事

m_rn
SE6年目。Swift2年目突入。技術に明るいエンジニアに憧れる日々。今は主にSwiftを業務でやるかたわら(2019秋〜)、お家でAlexaスキル開発やAWSを使ってみたりしている。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away