⌨️

Swift で画面スクロールした時にキーボードを閉じるようにする実装

2021/03/17に公開

今回は画面スクロールした時にキーボードが閉じられるようにする実装方法を備忘録として残しておこうと思います。(LINE や Slack の UI がイメージに近く、キーボードが出ている状態で下に画面をスクロールしようとすると表示されている キーボードを閉じることができる実装方法になります)

どのような View か

View のコードのイメージとしては以下のような状況です。

TextInputView.swift
final class TextInputView: UIView {
  @IBOutlet weak var textView: UITextView!
  
  // ...
}
ScrollableViewController.swift
final class ScrollableViewController: UIViewController {
  @IBOutlet private weak var scrollView: UIScrollView!
  private var textInputView = TextInputView()
  // ...
  override func viewDidLoad() {
    super.viewDidLoad()
    // ...
  }

  override var inputAccessoryView: UIView? {
    return textInputView
  }
  
  override var canBecomeFirstResponder: Bool {
    return true
  }
  
  // ...
}

かなり省略してしまっていますが、ViewController が ScrollView で覆われていて、inputAccessoryView として TextInputView という独自の View を持っている形になります。
inputAccessoryView はキーボードの上にくっつく View のようなもので、今回の場合はカスタム TextView のようなものを inputAccessoryView として指定しています。

どうやってスクロールできるようにするか

keyboardDismissMode を使った簡単な実装方法

実装方法は以下のようなイメージです。

ScrollableViewController.swift
final class ScrollableViewController: UIViewController {
  @IBOutlet private weak var scrollView: UIScrollView!
  private var textInputView = TextInputView()
  // ...
  override func viewDidLoad() {
    super.viewDidLoad()
    // これを追加するだけで終わり
    scrollView.keyboardDismissMode = .interactive
    // ...
  }

  override var inputAccessoryView: UIView? {
    return textInputView
  }
  
  override var canBecomeFirstResponder: Bool {
    return true
  }
  
  // ...
}

追加したのは ScrollView の keyboardDismissMode.interactive にしているコードだけです。
これだけで、ScrollView をスクロールした時に指が inputAccessoryView にかかったあたりでキーボードが閉じられるようになります。

UITapGestureRecognizer を使った少し面倒な実装方法

異なる実装方法として UITapGestureRecognizer を使ったパターンもありそうです。

実装方法は以下のようなイメージです。

ScrollableViewController.swift
final class ScrollableViewController: UIViewController {
  @IBOutlet private weak var scrollView: UIScrollView!
  private var textInputView = TextInputView()
  // ...
  override func viewDidLoad() {
    super.viewDidLoad()
    configureGesture()
    // ...
  }

  override var inputAccessoryView: UIView? {
    return textInputView
  }
  
  override var canBecomeFirstResponder: Bool {
    return true
  }
  
  // ...
}

private extension ScrollableViewController {
  func configureGesture() {
    // onPan という function を panGesture として登録する
    scrollView.panGestureRecognizer.addTarget(self, action: #selector(onPan(gesture:)))
    // keyboardDismissMode は .none にしておいて誤動作を起こさないようにする
    scrollView.keyboardDismissMode = .none
  }

  @objc
  func onPan(gesture: UIPanGestureRecognizer) {
    guard gesture.state == .changed else { return }
    // view 中の gesture の位置を取得
    let location = gesture.location(in: view)
    // inputAccessoryView の場合は superView に対する位置を取得してあげないとうまくいきませんでした(良い方法はあるかもしれないです🙏)
    guard let textInputViewRectForSuperView = textInputView.superView?.convert(textInputView.bounds, to: nil)
    // もし指が TextInputView にかかり始めたら resignFirstResponder でキーボードを閉じる
    if textInputViewRectForSuperView.contains(location) {
      textInputView.textView.resignFirstResponder()
    }
  }
}

おわりに

他にも実装方法はあるかもしれませんが、とりあえず「keyboardDismissMode を使った簡単な実装方法」を使っておくのが無難そうです🙏

参考

https://stackoverflow.com/questions/30042352/dismiss-keyboard-with-swipe-gesture

Discussion