SwiftUI.TextField に formatter を指定した場合、return を押さないと値が反映されない問題への対処
概要
SwiftUI.TextField
には formatter を指定する便利なイニシャライザが存在します。
しかし、このイニシャライザを使った場合は、return キーを押すタイミングでしか値が反映されないため、return を押さずにキーボードを閉じたり、他の入力欄に移動するなどした場合、入力していた内容が @State
に反映されていない状態になってしまいました。
この記事ではその問題への対処方法を記載します。
検証環境
- iOS 14.6
- Xcode 12.5
問題のサンプル
例えば、以下のように TextField
にカンマ区切りの金額を表示したいとします。
Submit を押すと入力欄のフォーカスが外れます。このタイミングで、@State var value: Int
に 値が反映されて欲しいのですが、反映されません。
2000
と入力して return を押すと 2,000 円
となってくれますが、3000
と入力して return を押さずに Submit を押すと、入力は終了しているのに、TextField は 3000
と表示されており、Value: 2000
と Text
に表示されているように、value
変数には反映されていません。
import SwiftUI
private let numberFormatter: NumberFormatter = {
let f = NumberFormatter()
f.locale = Locale(identifier: "ja_JP")
f.numberStyle = .currencyPlural
f.isLenient = true
return f
}()
struct ContentView: View {
@State var value: Int = 1000
var body: some View {
VStack {
TextField(
"金額",
value: $value,
formatter: numberFormatter,
onCommit: { print("commit") /* return を押すと onCommit が呼ばれる */ }
)
Text("Value: \(value.description)")
Button("Submit") {
UIApplication.shared.endEditing()
}
}
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
対処方法
良くない副作用があるかもしれませんが、強制的に return を押した時と同じイベントを発生させることができないかと考え、以下のように UITextField
を取得し、UIControl.Event.editingDidEndOnExit
を送信してみました。
struct ContentView: View {
@State var value: Int = 1000
var body: some View {
VStack {
TextField(
"金額",
value: $value,
formatter: numberFormatter,
onCommit: { print("commit") /* textField.sendActions(for: .editingDidEndOnExit) を呼ぶと onCommit が呼ばれる */ }
)
Text("Value: \(value.description)")
Button("Submit") {
UIApplication.shared.endEditing()
}
}
.textFieldStyle(RoundedBorderTextFieldStyle())
// 編集終了時のイベント
.onReceive(
NotificationCenter.default.publisher(for: UITextField.textDidEndEditingNotification),
perform: textDidEndEditing
)
}
func textDidEndEditing(_ notification: Notification) {
let textField = notification.object as! UITextField
// 以下を実行することで return を押した時と同じく onCommit が呼ばれている
textField.sendActions(for: .editingDidEndOnExit)
}
}
上記のままだと、onCommit
が呼ばれてほしくない TextField
にも適用されてしまうため、何らかの工夫が必要かもしれません。以下は onCommit
を強制的に行いたい場合にフラグを立てる例です。
struct ContentView: View {
@State var value: Int = 1000
// onCommit を強制的に呼び出したい場合にフラグを立てる
@State var shouldCommit = false
var body: some View {
VStack {
TextField(
"金額",
value: $value,
formatter: numberFormatter,
onEditingChanged: { editing in
if !editing {
// この入力欄は強制的に onCommit を発生させたい
shouldCommit = true
}
},
onCommit: { print("commit") }
)
Text("Value: \(value.description)")
Button("Submit") {
UIApplication.shared.endEditing()
}
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.onReceive(
NotificationCenter.default.publisher(for: UITextField.textDidEndEditingNotification),
perform: textDidEndEditing
)
}
func textDidEndEditing(_ notification: Notification) {
guard shouldCommit else { return }
shouldCommit = false
let textField = notification.object as! UITextField
textField.sendActions(for: .editingDidEndOnExit)
}
}
まとめ
TextField
の formatter を使った場合に、値が @State
に反映されない場合があることがわかりました。そのため、UITextField
を取り出し、sendActions(for: .editingDidEndOnExit)
を実行することで、@State
に値を反映させることができました。
あまり良い方法ではないと思いますが、どうしても困った場合、何かの参考になれば幸いです。
Discussion