はじめに
よくある入力フォームで、テキストフィールドをタップしたときには、キーボードに隠れないように、自動的にテキストフィールドが上にスクロールしてほしいですよね。
コーディングしなくても勝手にキーボードを回避してくれる機能はSwiftにはありませんので、自分で実装する必要があります。
世の中いろいろサンプルが出回っていますが、自分なりにようやく書けるようになったので、ここにまとめます。
当記事のコードの特徴
- テキストフィールドにタッチすると、
- キーボードに隠れない場合は、何も起きません。
- キーボードに隠れる場合は、テキストフィールドがキーボードの上辺に合わせてスクロールします。
- ユーザーが入力を完了すると、キーボードが下がるのに合わせて、テキストフィールドが元の位置にスクロールします。
- 対象のテキストフィールドがアクティブなときに、キーボードツールバーに上/下ボタンや完了ボタンを設定します。
実装イメージ
コードの説明
ここで説明するコードは、以下のリポジトリで公開しています。
https://github.com/mnaruse/KeyboardUpDownSample
Storyboard での注意点
UITextFieldを、UIScrollViewの中に設置しておく必要があります。
IBOutlet での注意点
大抵の場合、複数のUITextFieldが置いてあると思いますので、[UITextField] というUITextFieldsの配列を作っておきます。
@IBOutlet private var textFields: [UITextField]!
ViewControllerのファイルと、Storyboardのファイルを並べて開き、このコード上のIBOutletから、Storyboard内の対象のUITextFieldに向かって、接続していきます。
最終的にこんな感じになります。
キーボードのサイズが変化すると実行されるイベントハンドラー のコードのポイント
キーボードのサイズが変化すると実行される関数について、ポイントを説明します。
/// キーボードのサイズが変化すると実行されるイベントハンドラー
/// テキストフィールドが隠れたならスクロールする。
///
/// キーボードの退場でも同じイベントが発生するので、編集中のテキストフィールドが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)
}
}
コード全体
コードの全体は以下の通りです。
なお、コード内で実装するかしないか任意な点は以下の通りです。
- 対象のテキストフィールドがアクティブなとき、キーボードのツールバーに、上下ボタンや完了ボタンを設定するかどうかは任意です。
- この部分→
addPreviousNextableDoneButtonOnKeyboard(textFields: textFields, previousNextable: true)
- UIViewControllerのExtensionを作っていますので、よかったら、以下の記事でご覧ください。
- UIToolBarに上下ボタンや完了ボタンをつけるExtension
- この部分→
- ビューがタップされた時に、キーボードを下げるかどうかは任意です。
コード全体
//
// 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
}
}
おわりに
読んでいただきありがとうございました。
何か少しでもお役に立てたら嬉しいです
「こういうやり方もあるよ」とか「ここはこうした方がいいよ」とか、コメントやアドバイス、あるいは質問があれば、よかったらコメントお願いします。