SwiftUI Tips: 如何Binding一个可选值

3,803 阅读1分钟

使用 Binding 的时候我们经常会遇到需要绑定一个 Optional value 的情况。比如一个用于输入用户名的 TextField,如果是老用户就直接显示已有的用户名,新用户则为空。

struct BindingOptional: View {
    @State var userName: String? = nil
    
    var body: some View {
        TextField("UserName", text: $userName)
    }
}

这样写毫无疑问是要报错的,因为 Binding 的目标必须是一个确定的值。

Image.png

但是如果特意再声明一个 @State 做为中继,则繁琐很多。如果这样做代码大概要这么写:

struct BindingOptional: View {
    @State var userName: String? = nil
    @State var userNameTemp: String = "none"

    var body: some View {
        TextField("UserName", text: $userNameTemp)
            .onChange(of: userNameTemp) {
                self.userName = userNameTemp
            }
    }
}

因此找到一种方式可以便利针对 optional value的情况提供一个默认值很有必要。

自定义一个 Binding

@Binding 本质上是一个 property wrapper,最底层的思路就是我们把 Binding<T?> 封装成 Binding<T> 返回。

实现的代码如下:

struct BindingOptional: View {
    @State var userName: String? = nil
   
    private var userNameTemp: Binding<String> {
        Binding(get: {
            "none"
        }, set: { newValue in
            self.userName = newValue
        })
    }

    var body: some View {
        TextField("UserName", text: userNameTemp)
    }
}

我们可以在 get 闭包中返回默认值。

更进一步,封装成方法

但是这样写还是有点繁琐,我们可以把这个封装的过程定义为 Binding 的一个扩展方法。

extension Binding {
    func defaultValue<T>(_ value: T) -> Binding<T> where Value == Optional<T> {
        Binding<T> {
            wrappedValue ?? value
        } set: {
            wrappedValue = $0
        }
    }
}

我们就可以这样调用:

var body: some View {
        TextField("UserName", text: $userName.defaultValue("none"))
    }

考虑到我们最常使用的是字符串,因此我们可以针对字符串封装一个默认值是空字符的方法。

extension Binding where Value == Optional<String> {
    public var orEmpty: Binding<String> {
        Binding<String> {
            wrappedValue ?? ""
        } set: {
            wrappedValue = $0
        }
    }
}

这样我们使用的时候就会更简便一些:

var body: some View {
    TextField("UserName", text: $userName.orEmpty)
}

自定义运算符

除了定义成一个方法,也可以通过自定义运算符实现。因为 ?? 运算符的语义本来就代表提供默认值。如果想要更简洁一点把这个方法定义成 binding 的运算符也很合适。

extension Binding {
    static func ?? <T>(optional: Self, defaultValue: T) -> Binding<T> where Value == Optional<T> {
        Binding<T>(
            get: { optional.wrappedValue ?? defaultValue },
            set: { optional.wrappedValue = $0 }
        )
    }
}

我们就可以这样调用:

var body: some View {
    TextField("UserName", text: $userName ?? "none")
}