iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🔀

[SwiftUI] Enabling type conversion during data binding

に公開4

I don't know the general name for it, but it's troublesome when type conversion during data binding becomes cumbersome.

The most straightforward example is when inputting a numerical value using a TextField.

struct ContentView: View {
    @State private var number = 0
    var body: some View {
        // Cannot do this!
        TextField("Enter a number", text: $number)
    }
}

While there is an option to use a Formatter in this scenario, it's not universally applicable.
There are many other situations where type conversion is needed, such as using UIColor with a ColorPicker.

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

The method I wrote about in the previous article used subscript, but if it's just for type conversion, I think it should be possible to write it a bit more easily. So, let's achieve this by adding a method to Binding.

converted Method

Add the following function via an 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)
            }
        )
    }
}

How can this be used? Let's try it with the TextField example above.

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

The function specified for forward takes an Int and returns a String, and the function for backward takes a String and returns an Int. This is all it takes to enable type conversion.

Alternatively, you could write it like this. It looks quite clean.

struct ContentView: View {
    @State private var number = 0
    var body: some View {
        TextField("Enter a number", 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
    }
}

If the conversion can be done using initializers that cannot fail, such as between Color and UIColor, the description becomes even simpler.

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

I believe this approach is the cleanest specifically for type conversion.

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 🤖

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