LoginSignup
5
5

More than 3 years have passed since last update.

【iOS】画面をタップしたらキーボードを閉じる処理を共通化してみる

Last updated at Posted at 2020-05-09

はじめに

UIView の endEditing(_:) は特定のテキストフィールドによらずにキーボードを閉じる方法として有効です。

次のような実装で、画面のタップでキーボードを閉じることができます。

import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(UITapGestureRecognizer(
            target: self,
            action: #selector(endEditing(sender:))))
    }

    @objc func endEditing(sender: Any) {
        view.endEditing(true)
    }
}

しかし、このコードでは『画面のタップでキーボードを閉じる』要件を満たすには一般的には不完全です。

view.endEditing(true)

たとえば、ナビゲーションコントローラーを使用していて、表示中の画面ではナビゲーションバーを表示しているとします。すると、ナビゲーションバーをタップしてもキーボードを閉じることはできません。

これは、ナビゲーションバーがビューコントローラの管理するビューの階層配下に存在しないことに起因しています。

Screen_Shot_2020-05-09_at_18_44_46.png

ナビゲーションバーにも UITapGestureRecognizer を追加すれば、表示中のキーボードを閉じることはできますが、同じようなコードを何度も記述するのは面倒です。

これをできる限り共通化してみたいと思います。

UIGestureRecognizer をレスポンダチェイン可能に拡張する

UIGestureRecognizer は target, action の2つのパラメータを指定して初期化します。

UITapGestureRecognizer(target: self, action: #selector(....))

2つのパラメータに nil を渡すことが可能なので、一見すると nil-target action が機能するように見えますが、有効な値として機能しません。

これを次のように初期化した場合は、レスポンダチェインを辿って、ファーストレスポンダがセレクタを実行するように拡張してみます。

// target を指定できないようにして、レスポンダチェインでセレクタを実行させる
UITapGestureRecognizer(#selector(....))

次のような実装を行います。

レスポンダチェインでセレクタを実行させるサンプルコード
import UIKit
import ObjectiveC

public extension UIGestureRecognizer {
    convenience init(_ selector: Selector, cancelsTouchesInView: Bool = false) {
        self.init(Target(selector))
        self.cancelsTouchesInView = cancelsTouchesInView
    }

    private convenience init(_ target: Target) {
        self.init(target: target, action: #selector(Target.action(sender:)))
        objc_setAssociatedObject(self, &Target.associatedKey, target, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }

    private final class Target {
        static var associatedKey: Void = ()

        private let selector: Selector

        init(_ selector: Selector) {
            self.selector = selector
        }

        @objc func action(sender: Any) {
            UIApplication.shared.sendAction(selector, to: nil, from: nil, for: nil)
        }
    }
}

これを使って共通化します。

グローバルな共通化

AppDelegate または、SceneDelegate のウィンドウの初期化時に、次のようにして UIGestureRecognizer を追加します。こうすることで、ファーストレスポンダが resignFirstResponder を実行します。

AppDelegateの場合
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow? {
        didSet {
            window?.addGestureRecognizer(
                UITapGestureRecognizer(#selector(resignFirstResponder)))
        }
    }
}
SceneDelegateの場合
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow? {
        didSet {
            window?.addGestureRecognizer(
                UITapGestureRecognizer(#selector(resignFirstResponder)))
        }
    }
}

たったこれだけの実装で画面をタップするとキーボードが閉じるようになります。

ビューコントローラもナビゲーションバーも、通常はウィンドウの階層配下なので、
個別のビューに対して、UIGestureRecognizer の追加は不要になるわけです。

Untitled.mov.gif

処理が共通化されてナビゲーションバーなども含めて、どこをタップしてもキーボードが閉じるようになりました 🎉

特定のビューだけに制限した共通化

とはいえ、ウィンドウにタップジェスチャーを追加してしまうと、柔軟な対応ができなかったりと面倒な場合もあるので、特定のビューだけに制限してタップジェスチャーを追加したい場合もあると思います。

その場合は次のようなカスタムビューを用意して、インターフェースビルダーやコードでこれら(またはサブクラス)を配置すると良いでしょう。

CustomView.swft
class CustomView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        addGestureRecognizer(UITapGestureRecognizer(#selector(resignFirstResponder)))
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addGestureRecognizer(UITapGestureRecognizer(#selector(resignFirstResponder)))
    }
}
CustomNavigationBar.swift
class CustomNavigationBar: UINavigationBar {
    override init(frame: CGRect) {
        super.init(frame: frame)
        addGestureRecognizer(UITapGestureRecognizer(#selector(resignFirstResponder)))
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        addGestureRecognizer(UITapGestureRecognizer(#selector(resignFirstResponder)))
    }
}

これらのビューをタップするとキーボードが閉じます。

ビュー階層とレスポンダチェインには関係性が無いので、ナビゲーションバーに対して上記の UITapGestureRecognizer を追加してもファーストレスポンダが resignFirstResponder を処理します。

これで、ビューコントローラからキーボードを閉じるボイラープレートを消すことができます。

関連記事

【iOS】UITextField のキーボードを閉じる処理について

5
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5