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

## はじめに

UITextField はファーストレスポンダになるとキーボードを表示し、ファーストレスポンダをやめるとキーボードを非表示にする性質があります。

そのため、キーボードの表示・非表示は、ファーストレスポンダであるか否かで制御することになります。

```swift
// UITextField をファーストレスポンダにする（その結果、キーボードが表示される）
textField.becomeFirstResponder()
```

```swift
// UITextField のファーストレスポンダをやめる（その結果、キーボードが非表示になる）
textField.resignFirstResponder()
```

なのですが、ファーストレスポンダをやめる方法としては、他にも次のような方法があります。

+ `.editingDidEndOnExit` を Target-Action に追加する
+ `endEditing(_ force: Bool)` を呼び出す

これらの方法は `resignFirstResponder()` と何が違うのか？
気になったので、調べてみました。

#### 環境

+ Xcode Version 10.1 (10B61)
+ Simlators iOS 12.1 (16B91)

## `.editingDidEndOnExit`

**`.editingDidEndOnExit` は Return キーの入力によってファーストレスポンダをやめる場合に使用します。**

### サンプルコード

次のようなコードで、UITextField に Target-Action を追加します。

``` swift
import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Target-Action を追加する
        textField.addTarget(self, action: #selector(onExitAction(sender:)), for: .editingDidEndOnExit)
    }

    /// `.editingDidEndOnExit` イベントが送信されると呼ばれる
    @objc func onExitAction(sender: Any) {
    }
}
```

これで Return キーのタップでファーストレスポンダをやめることができます。

### `resignFirstResponder()` との関係

`.editingDidEndOnExit` イベントが送信されると、UIKit の内部処理で `resignFirstResponder()` が呼ばれるようになり、ファーストレスポンダをやめるようです。

次の場合は、Return キーをタップしても `resignFirstResponder()` は呼ばれません。

+ `addTarget(_:action:for:)` で `.editingDidEndOnExit` を追加していない
+ `removeTarget(_:action:for:)` で `.editingDidEndOnExit` をはずす

### ファーストレスポンダをやめるのをやめる

オブジェクトの状態などによっては、キーボードを閉じることを抑止したいということも、もしかしたらあるかもしれません。

`textFieldShouldReturn(_:)` で `false` を返すと、`.editingDidEndOnExit` イベントは送信されず、キーボードを表示したままにすることが出来ます。

```swift
import UIKit

final class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        // UITextFieldDelegate を設定する
        textField.delegate = self

        textField.addTarget(self, action: #selector(onExitAction(sender:)), for: .editingDidEndOnExit)
    }

    @objc func onExitAction(sender: Any) {
        // textFieldShouldReturn(_:) で `false` を返した場合は呼出されない
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        // `false` を返した場合は、`.editingDidEndOnExit` イベントは送信されない
        return false
    }
}
```

また、`textFieldShouldEndEditing(_:)` で `false` を返すことで、キーボードを表示したままにすることが出来ます。

`textFieldShouldEndEditing(_:)` は `resignFirstResponder()` の後で呼ばれるので、Return キーのタップに限らず `resignFirstResponder()` をキャンセルする目的で使用するとができます。

### 処理の流れ（概要）

以上の大まかな処理の流れを図に示すと、次のようになります。

![fc2-5.png](https://qiita-image-store.s3.amazonaws.com/0/63842/91fd3289-0a2c-62e1-28e9-b7ba181fbca7.png)

## `endEditing(_:)`

**`endEditing(_:)` はファーストレスポンダをやめるための便利なメソッドです。**
リターンキーのタップではなく、コードから直接ファーストレスポンダをやめる場合に使用します。

### サンプルコード

次のコードは、ビューをタップした際に、`endEditing(_:)` でファーストレスポンダをやめる実装例です。

複数の UITextField が配置されているビューでは `endEditing(_:)` を使用することで、
**ファーストレスポンダの探索を省いて、ファーストレスポンダをやめることができるので便利です。**

```swift
import UIKit

final class ViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        // `view` のタップジェスチャーを受け取る
        view.addGestureRecognizer(UITapGestureRecognizer(
            target: self,
            action: #selector(onTapAction(sender:))))
    }

    /// タップジェスチャーのアクション
    @objc func onTapAction(sender: UITapGestureRecognizer) {
        // `view` のサブビュー階層内のファーストレスポンダを探索して、
        // 該当オブジェクトの resignFirstResponder が呼び出される。
        view.endEditing(true)
    }
}
```

ちなみに、ファーストレスポンダの探索を省いて、ファーストレスポンダをやめるだけなら `endEditing(_:)` の代わりに次のような方法でも実現できます。

```swift
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
```

`sendAction(_:to:from:for:)` と `endEditing(_:)` は、引数や戻り値が異なるので、意図に合わせて使い分けると良さそうです。

### `resignFirstResponder()` との関係

`endEditing(_:)` を呼び出すと、UIKit の内部処理で `resignFirstResponder()` が呼ばれるようになり、その結果、ファーストレスポンダをやめます。
渡す引数によって、`resignFirstResponder()` を呼び出すまでの処理の流れが変わるようです。

### `endEditing(_:)` の引数と戻り値

`endEditing(_:)` は、次のように Bool の引数を取り、Bool を返すメソッドです。

```swift
func endEditing(_ force: Bool) -> Bool
```

force パラメータの指定により、動作や戻り値が異なるようです。

#### 引数に `true` を設定した場合

+ ビュー階層内のファーストレスポンダを探索して `resignFirstResponder()` を呼ぶ
+ 戻り値は常に `true` を返す

#### 引数に `false` を設定した場合

+ まず、UITextFieldDelegate の `textFieldShouldEndEditing(_:)` を呼ぶ
  + `true` を返した場合は、`resignFirstResponder()` を呼ぶ
  + `false` を返した場合は `resignFirstResponder()` は呼ばれない
+ `endEditing(_:)` の戻り値は上述の `textFieldShouldEndEditing(_:)` の戻り値に一致する

force パラメータに `false` を設定することで、ファーストレスポンダをやめたかどうかを判定することができます。

```swift
import UIKit

final class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet private weak var textField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        // UITextFieldDelegate を設定する
        textField.delegate = self

        view.addGestureRecognizer(UITapGestureRecognizer(
            target: self,
            action: #selector(onTapAction(sender:))))
    }

    /// タップジェスチャーのアクション
    @objc func onTapAction(sender: UITapGestureRecognizer) {
        // force パラメータに `false` を設定した場合は
        // `textFieldShouldEndEditing(_:)` が `true` を返した場合のみ
        // `resignFirstResponder()` が呼出される
        let result = view.endEditing(false)
        if !result {
            // `resignFirstResponder()` は呼出されていないので
            // キーボードは閉じない
        }
    }

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        // `false` を返すと `resignFirstResponder()` は呼出されない
        return false
    }
}
```

### 処理の流れ（概要）

`endEditing(_:)` を呼び出した際の、大まかな処理の流れを図に示すと、次のようになります。

![fc2-6.png](https://qiita-image-store.s3.amazonaws.com/0/63842/0a91b349-254c-4880-47b5-d2c3ecd5639c.png)

注意すべき点があります。
`endEditing(_:)` に `false` を渡して、`textFieldShouldEndEditing(_:)` で `true` を返すと、結果として、 **`textFieldShouldEndEditing(_:)` は２回呼ばれます。**

また、この場合の `endEditing(_:)` の戻り値は、`textFieldShouldEndEditing(_:)` の１度目の呼び出しの戻り値に一致し、２度目の戻り値を無視するような挙動でした。

## まとめ・感想

基本的には `resignFirstResponder()` でファーストレスポンダをやめることが出来ますが、状況に応じた便利な仕組みが UIKit には提供されているみたいです。

+ Return キーで閉じる場合は `.editingDidEndOnExit`
+ コードからファーストレスポンダをやめる場合は `endEditing(_ force: Bool)` が便利

ともに、UIKit 内部で `resignFirstResponder()` を呼び出していて、`resignFirstResponder()` がファーストレスポンダをやめるための、共通処理になっていることが想像できました。

UIKit を利用する立場としては、ファーストレスポンダをやめるための便利な仕組みが提供されていると思って、使い所に合わせて、これらを利用すれば良さそうです。