[SwiftUI] データバインド時に型の変換を可能にする

2 min read読了の目安(約2000字 3

一般的な名称を知らないのですが、データバインドの際に型変換するのが厄介で困ります。

一番わかりやすい例はTextFieldで数値を入力する場面です。

struct ContentView: View {
    @State private var number = 0
    var body: some View {
        //できない!
        TextField("数値を入力", text: $number)
    }
}

この場面ではFormatterを用いる選択肢がありますが、一般的には使えません。
その他にもColorPickerUIColorを、というように、型変換が必要な場面は往々にしてあるものです。

https://zenn.dev/en3_hcl/articles/aef18575a96bec

以前この記事で書いた方法はsubscriptを用いるものですが、型変換だけならばもう少し楽にかけてもいいんじゃないかと思います。そこでBindingにメソッドを追加することでこれを実現してみましょう。

convertedメソッド

以下のような関数をextensionで追加します。

public extension Binding {
    func converted<T>(forward forwardConverter: @escaping (Value) -> T, backward backwardConverter: @escaping (T) -> Value) -> Binding<T> {
        .init(
            get: {
                return forwardConverter(self.wrappedValue)
            },
            set: {newValue in
                self.wrappedValue = backwardConverter(newValue)
            }
        )
    }
}

これがどう使えるのでしょうか。上のTextFieldの例でやってみましょう。

struct ContentView: View {
    @State private var number = 0
    var body: some View {
        TextField("数値を入力", text: $number.converted(forward: {String($0)}, backward: {Int($0) ?? 0}))
    }
}

forwardに指定したのはIntを受け取ってStringを返す関数、backwardに指定したのはStringを受け取ってIntを返す関数です。これだけで型変換が可能になります。

あるいは以下のように書いてもいいでしょう。スッキリしていて良さそうです。

struct ContentView: View {
    @State private var number = 0
    var body: some View {
        TextField("数値を入力", text: $number.converted(forward: toString, backward: toInt))
    }
    private func toString(value: Int) -> String {
        return String(value)
    }
    private func toInt(value: String) -> Int {
        return Int(value) ?? 0
    }
}

ColorUIColorのように相互の変換が失敗不可能なイニシャライザで行えるなら、記述はさらにシンプルになります。

struct ContentView: View {
    @State private var color = UIColor.black
    var body: some View {
        ColorPicker("色を選択", selection: $color.converted(forward: Color.init, backward: UIColor.init))
    }
}

型変換に限ってはこの方式が一番綺麗じゃないかな、と考えています。