執筆者: 霽月
最終更新: 2023/03/28
霽月です。Swiftちょっと分かる人向け(≠チョットデキル)の記事です。
Appleのメモ帳アプリなどでは、改行しまくってキーボードに迫ったとしても、キーボードの下にカーソルが隠れることなく、自動的にスクロールされることで、カーソルが常に見える位置に存在するようになっていますよね。
その機能を再現する方法についてネットで少し調べてみましたが、あまり良い解決策は見つかりませんでした。ということで、この記事では、単純で分かりやすくその機能を実現する方法について紹介します。
まずは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
}
実行してエラーがないことを確認して、次の段階に進みます。
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)
}
}
この人が書いた記事