SwiftUI.TextField に returnKeyType を設定するために Notification を活用する

3 min read読了の目安(約3200字

概要

SwiftUI で TextFieldreturnKeyType (UIReturnKeyType) を設定する標準的な API がなかったため、TextField の裏側で使われている UITextField を取り出して設定する方法の紹介です。

検証環境

  • iOS 14.6
  • Xcode 12.5

UITextField を取り出す方法

よく見かける方法としては SwiftUI-Introspect のようなライブラリを使い、View 階層を辿って UITextField を取り出す方法がありますが、この記事では Notification を利用した方法を紹介します。

UITextField.textDidBeginEditingNotification を利用する

UITextField.textDidBeginEditingNotification が UIKit に定義されているので、それを使えば TextField の入力開始時に、UITextField を取得できます。

以下の方法だと、SwiftUI の裏側の View 構造を気にせずに UITextField を取得できます。

struct ContentView: View {
    @State var text = ""
    
    var body: some View {
        VStack {
            TextField("Field 1", text: $text)
        }
        .textFieldStyle(RoundedBorderTextFieldStyle())
        // 通知を受信する
        .onReceive(
            NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification),
            perform: textDidBeginEditing
        )
    }
    
    func textDidBeginEditing(_ notification: Notification) {
        // UITextField を取り出せる
        let textField = notification.object as! UITextField
        // returnKeyType を設定できる
        textField.returnKeyType = .done
    }
}

ちゃんと done になりました。

複数の TextField が存在する場合

上記の方法だけですと、複数の TextField が存在する場合、全ての TextField に適用されて困る場面があるので、何らかの方法で対象の TextField かどうかを判断する必要があります。

onEditingChanged と組み合わせる

ちょっと苦しい方法ですが、なかなか良い方法を思いつかなかったので、onEditingChanged と組み合わせる方法を記載します。

単純ですが、TextFieldonEditingChanged を使うことで、その入力欄の編集が開始されたか終了したかを取得できます。

これと、上記で紹介した Notification を組み合わせることで、どの入力欄で編集している最中に Notification が飛んできたかを判断することができます。

入力欄が二つあり、最後の入力欄のみ、returnKeyTypedone にするには、以下のように記述します。

struct ContentView: View {
    @State var text1 = ""
    @State var text2 = ""
    @State var focusedTextField: Int?
    
    var body: some View {
        VStack {
            TextField(
                "Field 1",
                text: $text1,
                onEditingChanged: { editing in
		    // どの入力欄かがわかる識別子を入れる
                    focusedTextField = editing ? 1 : nil
                }
            )
            
            TextField(
                "Field 2",
                text: $text2,
                onEditingChanged: { editing in
                    focusedTextField = editing ? 2 : nil
                }
            )
        }
        .onReceive(
            NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification),
            perform: textDidBeginEditing
        )
    }
    
    func textDidBeginEditing(_ notification: Notification) {
        let textField = notification.object as! UITextField
        
	// 識別子を判定して設定を行う
        if focusedTextField == 2 {
            textField.returnKeyType = .done
        }
    }
}

まとめ

UITextField.textDidBeginEditingNotification を利用することで、SwiftUI.TextField の裏側に存在する UIKit.UITextField を取得できました。
また、onEditingChanged コールバックを利用することで、どの TextField かを区別することもできました。

SwiftUI のみでうまくやる方法があれば良いですが、今回紹介した内容が何かの参考になれば幸いです。