はじめに
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)
たとえば、ナビゲーションコントローラーを使用していて、表示中の画面ではナビゲーションバーを表示しているとします。すると、ナビゲーションバーをタップしてもキーボードを閉じることはできません。
これは、ナビゲーションバーがビューコントローラの管理するビューの階層配下に存在しないことに起因しています。
ナビゲーションバーにも 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
を実行します。
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? {
didSet {
window?.addGestureRecognizer(
UITapGestureRecognizer(#selector(resignFirstResponder)))
}
}
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? {
didSet {
window?.addGestureRecognizer(
UITapGestureRecognizer(#selector(resignFirstResponder)))
}
}
}
たったこれだけの実装で画面をタップするとキーボードが閉じるようになります。
ビューコントローラもナビゲーションバーも、通常はウィンドウの階層配下なので、
個別のビューに対して、UIGestureRecognizer の追加は不要になるわけです。
処理が共通化されてナビゲーションバーなども含めて、どこをタップしてもキーボードが閉じるようになりました 🎉
特定のビューだけに制限した共通化
とはいえ、ウィンドウにタップジェスチャーを追加してしまうと、柔軟な対応ができなかったりと面倒な場合もあるので、特定のビューだけに制限してタップジェスチャーを追加したい場合もあると思います。
その場合は次のようなカスタムビューを用意して、インターフェースビルダーやコードでこれら(またはサブクラス)を配置すると良いでしょう。
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)))
}
}
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
を処理します。
これで、ビューコントローラからキーボードを閉じるボイラープレートを消すことができます。