🔀

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

2021/04/28に公開4

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

一番わかりやすい例は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))
    }
}

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

Discussion

Harumaru 🤖Harumaru 🤖

Bindingのイニシャライザinit(get:set:)を使う方法もありますよ。クロージャで読み書きの処理を指定できるし、コード量も少ないのでかなり応用が効くと思います。

import SwiftUI

struct NumberField: View {
    @State private var num = 0
    var body: some View {
        let binding = Binding(
            get: { String(num) },
            set: { num = Int($0) ?? 0 }
        )
        return TextField("Type Number...", text: binding)
    }
}
MiwaMiwa

ありがとうございます!
わかりにくかったかもしれないのですが、この記事のconvertedを用いた方法でも内部ではBindingのイニシャライザを利用しています。型変換を行うのはそこそこ一般的な要求なので、Viewの側で明示的にBindingを定義せずに済ませたいというのがモチベーションでした。

Harumaru 🤖Harumaru 🤖

もうちょっとよく読むべきでした、すいません😅
型変換をしっかり関数で書けるので、型変換の処理が複雑な場合などは記事で紹介されている方がわかりやすいですね。