[Swift] ScrollViewとAutoLayoutを利用してTextFieldやTextViewがキーボードに隠れないようにしよう!


はじめに

プロジェクトを作りことになるとほとんど使うことになるTextField、TextViewの入力パーツ。
入力するためにタップするとキーボードが上がってくるんですが、入力パーツが画面の下の部分にあるとキーボードによって隠れてしましますね。
今回はStoryBoardとAutoLayoutを利用しているプロジェクトでパーツがキーボードに隠れる問題が起きないようにしていきます。

View作成

例として配送情報を入力する画面を作りました。

ScrollView


今回はXCode11から追加されたScroll ViewのContent Layout GuidesはOffにします。
そしてScrollViewをControllerいっぱいに設定します。

ScrollViewの中のView

ScrollViewを利用するとき慣れてないと少し難しく感じるかもしれませんがシンプルに考えてScrollViewの中のViewがScrollView以上の大きさを持ってる場合スクロールするようになります。ScrollViewを画面いっぱいに設定したので中のViewが画面より長い場合縦スクロール、横幅が画面の横が幅より大きい場合横にスクロールします。
この原理をキーボードが出てきたとき利用するので覚えておいてください。


まず、中のViewをScrollViewいっぱいに設定します。

中のViewとcontrolキーを押したままScrollViewの方にドラグ&ドロップしてEqual Widthsを押して幅をScrollViewと同じにします。

中のViewの高さは中に配置するTextFieldなどで決めますが、その前までAutoLayoutのエラーが出てるのが気持ち悪いので仮で高さを1000設定します。(後で消します)
ここまででエラーは出なくなってStoryBoard場で縦スクロールが出来るようになっているか確認してみてください!

入力フォーム

配送情報を入力するフォームを作ります。

私の場合StackViewを利用しました。
中のフォームは自由に作ってください

出来上がった画面はこんな感じです!

それでは中のViewのパーツが決まったのでさっき仮で設定しておいた中のViewの高さを消します。
この制約をクリックしてバックスペースを押したら消えます。

消した後は中のViewの高さを改めて設定します。私の場合StackViewの一番下の部分に合わせてマージン100を設定しました。
一番下に置いたパーツを利用して制約を決めてください。
これで画面の設定は終わりです!

確認


シミュレーターで確認してみるとこんな感じで表示されています。
中のViewがScrollViewより小さい(縦)のでスクロールできない状態ですね。
それでは一番下のパーツを押してみます。

やっぱり隠れてしまいましたねー。
これではユーザーは永遠に入力できない状態です。
これからこの問題を解決していきましょう!

Outletを結びつける

これから利用する部分をコードで利用出来るようにします。
必要なパーツはこの二つになります。
1.ScrollView
2.ScrollViewのAutoLayoutのBottom制約

Bottom制約はこの部分をドラグ&ドロップするとコードで使えるようになります。

TextFieldのdelegateを利用してキーボードを下げるのでdelegateを利用可能にします。


それぞれ、TextFieldとcontrolキーを押してViewController側の丸いアイコンにドラグ&ドロップして全てのTextFieldのdelegateをOnにします。

隠れないようにコードを書く

いよいよコードの部分です。

キーボードの動きを感知するためのNotificationを設定とキーボードを下げるためのTextFiledのdelegateを設定

ViewController.swift
import UIKit

class ViewController: UIViewController {

  @IBOutlet weak var scrollView: UIScrollView!
  @IBOutlet weak var scrollViewBottomConstraint: NSLayoutConstraint!

  override func viewDidLoad() {
    super.viewDidLoad()

    setupNotifications()
  }

  private func setupNotifications() {
    //キーボードが表示される時呼ばれるNotification
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
    //キーボードが非表示になる時呼ばれるNotification
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
  }

  @objc private func keyboardWillChangeFrame(_ notification: Notification) {
    print("キーボード表示")
  }

  @objc private func keyboardWillHide(_ notification: Notification) {
    print("キーボード非表示")
  }
}


//MARK: - UITextFieldDelegate
extension ViewController: UITextFieldDelegate {
  //キーボードReturnキーを押したらキーボードを下げる
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    textField.resignFirstResponder()
  }
}

シミュレーターでキーボードが表示される時と非表示になる時ちゃんとprintされているのを確認できました。

キーボードが表示された時のメソッド

ここが今日の目的になります。

現在のScrollViewの高さは画面いっぱいの赤い枠になります。
そして、キーボードの高さが青い枠です。
このままだとキーボードの高さ分のScrollViewが隠れるためScrollViewの高さからキーボードの高さを引いてScrollViewの高さを再設定する必要があります。緑の枠が再設定する高さです。
緑の枠がScrollViewの大きさになったらScrollViewのなかのViewがScrollViewより大きくなるのでスクロールが可能になります!
それでは高さを際せってするためにコードをいじってみましょう。

ViewController.swift
@objc private func keyboardWillChangeFrame(_ notification: Notification) {
          //キーボードのサイズ
    guard let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue,
          //キーボードのアニメーション時間
          let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
          //キーボードのアニメーション曲線
          let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt,
          //Outletで結び付けたScrollViewのBottom制約
          let scrollViewBottomConstraint = self.scrollViewBottomConstraint else { return }

    //キーボードの高さ
    let keyboardHeight = keyboardFrame.height
    //Bottom制約再設定
    scrollViewBottomConstraint.constant = keyboardHeight

    //アニメーションを利用してキーボードが上がるアニメーションと同じ速度でScrollViewのたBottom制約設定を適応
    UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve), animations: {
      self.view.layoutIfNeeded()
    })
  }


AutoLayoutを利用しているのでScrollViewのBottom制約をキーボードの高さ分上にグイっと持ち上げるような感じでScrollViewの高さを再設定することができます。
シミュレーターで確認してみてください。
アニメーション付きでいい感じに高さが調整されてキーボードに隠れないようになったと思います👏👏👏👏

キーボードが非表示になる時のメソッド

それでは最後にキーボードが下がる時高さが小さくなったScrollViewの高さを画面いっぱいに直してあげる必要があります。

ViewController.swift
@objc private func keyboardWillHide(_ notification: Notification) {
          //キーボードのアニメーション時間
    guard let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
          //キーボードのアニメーション曲線
          let curve = notification.userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt,
          //Outletで結び付けたScrollViewのBottom制約
          let scrollViewBottomConstraint = self.scrollViewBottomConstraint else { return }

    //画面いっぱいになるのでBottomのマージンを0に戻す
    scrollViewBottomConstraint.constant = 0

    //アニメーションを利用してキーボードが上がるアニメーションと同じ速度でScrollViewのたBottom制約設定を適応
    UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve), animations: {
      self.view.layoutIfNeeded()
    })
  }
}


ScrollViewのBottom制約を0に戻してあげるだけで終了です!
シミュレーターで確認するとキーボードが上がる時も上がる時もアニメーションしながらいい感じにScrollViewの高さを調整していますね👏👏👏👏
お疲れ様でした!