トップ画像
UITextViewでキーボードがカーソルを隠さないようにする明快な方法

執筆者: 霽月

最終更新: 2023/03/28

内容

霽月です。Swiftちょっと分かる人向け(≠チョットデキル)の記事です。
Appleのメモ帳アプリなどでは、改行しまくってキーボードに迫ったとしても、キーボードの下にカーソルが隠れることなく、自動的にスクロールされることで、カーソルが常に見える位置に存在するようになっていますよね。
その機能を再現する方法についてネットで少し調べてみましたが、あまり良い解決策は見つかりませんでした。ということで、この記事では、単純で分かりやすくその機能を実現する方法について紹介します。

環境

  • macOS Ventura 13.2.1
  • シミュレータ: iPhone 14 Pro(iOS16.2)
  • Xcode14.2
  • Swift5.7.2


UITextViewを配置

まずはUITextViewを画面に配置します。

import UIKit

class ViewController: UIViewController {
    private let textView = UITextView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(textView)
        textView.frame = view.bounds
        textView.text = String(repeating: "これは UITextView を使ったテストです。\n", count: 100)
    }
}

実行すると適当な文章が100行表示されます。

キーボードの高さを取得

次に、キーボードが開かれた時に、キーボードの高さを取得するようにします。
保存先の変数を作って、

    private var keyboardHeight: Double = 0.0

以下をviewDidLoadの中に書いて、キーボードが表示された時にメソッドが呼び出されるように登録し、

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(_:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil)

キーボードの高さを取得する処理を書きます。

    @objc func keyboardWillShow(_ notification: Notification) {
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardHeight = keyboardFrame.height
    }

実行してエラーがないことを確認して、次の段階に進みます。

textViewDidChange関数

textViewに変更が加えられた時に呼び出される関数を作ります。
textViewのdelegateをselfにしておいて、

        textView.delegate = self

textViewDidChange関数の中に書いていきます。

extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        
    }
}


目的の機能を作成

必要な情報は「現在のスクロール具合」「カーソルがある矩形領域」「キーボードが開いてても見える矩形領域」です。この3つの情報を取得します。

        var contentOffset = textView.contentOffset
        let cursorPosition = textView.selectedTextRange?.start ?? textView.beginningOfDocument
        var cursorRect = textView.caretRect(for: cursorPosition)
        let visibleRect = CGRect(x: 0, y: 0, width: textView.bounds.width, height: textView.bounds.height - keyboardHeight)
        cursorRect.origin.y -= contentOffset.y

順に「contentOffset」「cursorRect」「visibleRect」という名前で取得しました。
カーソルの下端がvisibleRectの下端より下に行かないようにします。

        if cursorRect.maxY > visibleRect.maxY {
            contentOffset.y += cursorRect.maxY - visibleRect.maxY
        }

最後にcontentOffsetの変更を保存して終わりです。

        textView.setContentOffset(contentOffset, animated: true)


実行して確認

問題なく実行できました。カーソルが見えない場所に行ってしまう現象は解決され、とても便利なUITextViewの完成です!
と、思いきや?
textVIewのコンテンツの下端に余白がないために、下の方を編集しようとすると物凄くやりにくいことになってしまいました。
キーボードが開いている時は下端にキーボードの高さ分の余白を開けるようにしてみましょう。
keyboardWillShowに以下を追加します。

        textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
        textViewDidChange(textView)

キーボードが閉じた時の通知も登録して、

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(_:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil)

キーボードが閉じた時に余白を元に戻す処理を書きます。

    @objc func keyboardWillHide(_ notification: Notification) {
        textView.textContainerInset = UIEdgeInsets.zero
    }

これで完璧です。

最終的なコードの全文

import UIKit

class ViewController: UIViewController {
    private let textView = UITextView()
    
    private var keyboardHeight: Double = 0.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(textView)
        textView.frame = view.bounds
        textView.delegate = self
        textView.text = String(repeating: "これは UITextView を使ったテストです。\n", count: 100)
        
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(_:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide(_:)),
            name: UIResponder.keyboardWillHideNotification,
            object: nil)
    }
    
    @objc func keyboardWillShow(_ notification: Notification) {
        guard let userInfo = notification.userInfo else { return }
        guard let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
        keyboardHeight = keyboardFrame.height
        
        textView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
        textViewDidChange(textView)
    }
    
    @objc func keyboardWillHide(_ notification: Notification) {
        textView.textContainerInset = UIEdgeInsets.zero
    }
}

extension ViewController: UITextViewDelegate {
    func textViewDidChange(_ textView: UITextView) {
        var contentOffset = textView.contentOffset
        let cursorPosition = textView.selectedTextRange?.start ?? textView.beginningOfDocument
        var cursorRect = textView.caretRect(for: cursorPosition)
        let visibleRect = CGRect(x: 0, y: 0, width: textView.bounds.width, height: textView.bounds.height - keyboardHeight)
        cursorRect.origin.y -= contentOffset.y

        if cursorRect.maxY > visibleRect.maxY {
            contentOffset.y += cursorRect.maxY - visibleRect.maxY
        }

        textView.setContentOffset(contentOffset, animated: true)
    }
}
取得に失敗しました

2022年度 入部

Twitter GitHub