SwiftUIのTextFieldでOptionalなStringをbindする

SwiftUIのTextFieldにイニシャライザはinit(_ titleKey: LocalizedStringKey, text: Binding<String>)なので以下のように String? な値をbindするとエラーになります。

struct PlaygroundView: View {
    @State var text: String? = ""
    var body: some View {
        VStack {
            TextField("Text", text: $text)
        }
        .frame(width: 300, height: 300)
    }
}

PlaygroundPage.current.setLiveView(PlaygroundView())

実行すると Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding<String>' というエラーになります。

解決方法

TextFieldにはinit<F>(_ titleKey: LocalizedStringKey, value: Binding<F.FormatInput>, format: F, prompt: Text? = nil) where F : ParseableFormatStyle, F.FormatOutput == String という方のジェネリックなイニシャライザもあります。つまりこれを使えば Optional<String> も表現できるはずです。

こちらを使うためには format 引数に与えるための FormatStyle が必要です。(おそらくですが)String? → String に変換するものは標準で用意されていないので自分で作ります。

今回は PassThroughStringFormatStyle という名前にしました。この format メソッドの引数が String? になっているので入力値(FormatInput)の型が String? になります。

struct PassThroughStringFormatStyle: ParseableFormatStyle {

    var parseStrategy: Strategy = .init()
    func format(_ value: String?) -> String {
        value ?? ""
    }
    struct Strategy: ParseStrategy {
        func parse(_ value: String) throws -> String? {
            // 空のときにnilにしたかったら guard !value.isEmpty else { return nil } とする.
            // もしくは このStrategyが引数を受け取るようにして制御する.
            return value
        }
    }
}

これをTextFieldで使った結果が以下になります。ちゃんとエラーが出ずに実行できます。

struct PassThroughStringFormatStyle: ParseableFormatStyle {

    var parseStrategy: Strategy = .init()
    func format(_ value: String?) -> String {
        value ?? ""
    }
    struct Strategy: ParseStrategy {
        func parse(_ value: String) throws -> String? {
            // 空のときにnilにしたかったら guard !value.isEmpty else { return nil } とする.
            // もしくは このStrategyが引数を受け取るようにして制御する.
            return value
        }
    }
}

struct PlaygroundView: View {
    @State var text: String? = ""
    var body: some View {
        VStack {
            TextField("Text", value: $text, format: PassThroughStringFormatStyle())
        }
        .frame(width: 300, height: 300)
    }
}

TextField("Text", value: $text, format: .passThroughString) みたいに書きたい場合は extension を作って頑張るとできます。

以上です。