【Swift】TextViewがキーボードに隠れてしまう問題を解決する

2021.09.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

TextViewにテキストを入力していたら、テキストがキーボードの下にまで侵攻して何を入力しているのか分からないという問題が発生しました。

この問題を解決するべく、キーボードが出ている時はTextViewがキーボードに隠れない実装を行うことにしました。

完成形

今回はこの完成形を目指し進めていきます。

環境

  • Xcode 12.5
  • Swift 5.4

LayoutConstraintの紐付け

まずはtextViewconstraintsを上下左右とも0に設定しておきます。

そして、textViewとTextViewのBottomLayoutConstraintviewControllerに紐付けます。

対象のconstraintをドラッグ&ドロップすることでviewControllerと紐付けることが出来ます。

@IBOutlet weak private var textView: UITextView!
@IBOutlet weak private var textViewButtonConstraint: NSLayoutConstraint!

紐付けが出来たので次に進みましょう。

キーボードの表示非表示の通知を受け取る

まずはviewDidLoad内でNotificationCenteraddObserverを行い、キーボードのShow/ Hideの通知を受け取り、同時にセレクターの処理を呼べるようにします。

override func viewDidLoad() {
    super.viewDidLoad()
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(keyboardWillShow),
                                           name: UIResponder.keyboardWillShowNotification,
                                           object: nil)
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(keyboardWillHide),
                                           name: UIResponder.keyboardWillHideNotification,
                                           object: nil)
}

addObserverの引数nameでどんな通知を受け取るかを指定することができるのですが、今回はキーボードが「これから出る時」「これから閉じる」の通知を受け取りたいので、UIResponder.keyboardWillShowNotificationUIResponder.keyboardWillHideNotificationをそれぞれ指定しました。

selectorで指定しているメソッドも追加しておきます。

@objc private func keyboardWillShow(_ notification: Notification) {
    // キーボードが現れる時の処理
}

@objc private func keyboardWillHide(_ notification: Notification) {
    // キーボードが閉じる時の処理
}

キーボードが出る時の処理を実装する

今回はtextViewに入力した文字がキーボードが隠れないようにする為に、キーボードが出ている時はtextViewをキーボードの高さ分だけtextViewBottomを上げるという処理で対応することにしました。

キーボードに関する値を取得する

まずは受け取ったnotificationから今回はキーボードの表示非表示に関する値を取り出すことができるので、Notificationextensionを作成し、キーボードの高さキーボードのアニメーション時間キーボードのアニメーション曲線の値を持つ変数を作成します。

extension Notification {

    // キーボードの高さ
    var keyboardHeight: CGFloat? {
        return (self.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height
    }

    // キーボードのアニメーション時間
    var keybaordAnimationDuration: TimeInterval? {
        return self.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval
    }

    // キーボードのアニメーション曲線
    var keyboardAnimationCurve: UInt? {
        return self.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt
    }
}

keyboardFrameEndUserInfoKey

Notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey]NSValueでプロパティとしてキーボードの長方形のCGRectを持っています。今回はそのCGRectからキーボードの高さだけを取り出して変数に渡しています。

keyboardAnimationDurationUserInfoKey

Notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey]はキーボードのアニメーション時間を持っているので、TimeIntervalにダウンキャストして変数に渡しています。

keyboardAnimationCurveUserInfoKey

Notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey]はキーボードのUIView.AnimationCurveを持っており、これはキーボードがどのようなアニメーションカーブでアニメーションするかを定義しています。キーボードの動きに合わせてtextViewの高さを変更したいのでこの値も変数に渡しています。

textViewのBottomをキーボードの高さ分だけ上げる

@objc private func keyboardWillShow(_ notification: Notification) {

    guard let keyboardHeight = notification.keyboardHeight,
          let keyboardAnimationDuration = notification.keybaordAnimationDuration,
          let KeyboardAnimationCurve = notification.keyboardAnimationCurve
    else { return }

    UIView.animate(withDuration: keyboardAnimationDuration,
                   delay: 0,
                   options: UIView.AnimationOptions(rawValue: KeyboardAnimationCurve)) {
        // アニメーションさせたい実装を行う
        self.textViewButtonConstraint.constant = keyboardHeight
    }
}

Notificationから受け取ったキーボードに関する各値UIView.animateの引数に渡しています。

  • withDuration: アニメーションの持続時間
  • delay: アニメーションの遅延時間
  • options: アニメーションオプション
  • animations: アニメーションするviewの実装

animationsのコードブロック内に、textViewButtonConstraint.constantkeyboardHeightの値を代入することでTextViewBottomがキーボードの高さ分だけ上がってくれます。

キーボードが隠れる時の処理を実装する

基本的にはキーボードが出る処理とほとんど変わりませんが、キーボードが閉じた時、いわばTextViewを全面に表示したい時なので、UIView.animateanimationsのコードブロック内で、textViewButtonConstraint.constantの値を0にしてあげるだけで全体に表示されるようになります。

@objc private func keyboardWillHide(_ notification: Notification) {
    guard let keyboardAnimationDuration = notification.keybaordAnimationDuration,
          let KeyboardAnimationCurve = notification.keyboardAnimationCurve
    else { return }

    UIView.animate(withDuration: keyboardAnimationDuration,
                   delay: 0,
                   options: UIView.AnimationOptions(rawValue: KeyboardAnimationCurve)) {
        self.textViewButtonConstraint.constant = 0
    }
}

おまけ

今回の実装では、キーボードの高さ分だけtextViewを上げているつもりですが、実はBottomsafeAreaInset分も考慮されたまま上がっています。その為、キーボードとtextViewの間にやや隙間が出来てしまいます。

この問題は、キーボードの高さからBottomsafeAreaInsetの高さを引いてあげると解消出来ます。

UIView.animate(withDuration: keyboardAnimationDuration,
               delay: 0,
               options: UIView.AnimationOptions(rawValue: KeyboardAnimationCurve)) {
    self.textViewBottomConstraint.constant = keyboardHeight - self.view.safeAreaInsets.bottom
}

コード

今回のコードはGitHubに載せております。

おわりに

希望通りの動きを実装することができました。また、keyboardWillShowkeyboardWillHideの中の実装があまり違いがないのでリファクタリングするのも良さそうですね。

この記事が誰かの助けになれれば幸いです☺︎