SwiftUI 可选文本框

64

SwiftUI的文本字段能够使用可选绑定吗?目前,此代码:

struct SOTestView : View {
    @State var test: String? = "Test"

    var body: some View {
        TextField($test)
    }
}

产生以下错误:

无法将类型为'Binding< String?>'的值转换为预期的参数类型'Binding< String>'

有没有什么方法可以解决这个问题?在数据模型中使用可选项是一种非常常见的模式——事实上,它是Core Data中的默认设置,因此SwiftUI不支持它们似乎很奇怪。


2
你可以使用空字符串代替吗?@State var test = ""?如果不行,那么当你的字符串为nil时,你想要TextField做什么? - user7014451
4
是的 - 问题出在 Core Data 上,它将 NSManaged Strings 创建为可选类型。 - Brandon Bradley
好的,使用String?,你可以认为nil""是“等价的”,但如果你的数据模型包含CLLocation?-什么是nil的“等价物”?SwiftUI通常不与Optional很好地配合。 - Grimxn
5
提交了一个 Feedback Assistant 报告,请求可选绑定功能,编号为 FB7619680。 - Curiosity
1
在模型中为属性设置默认字符串。 - malhal
显示剩余2条评论
7个回答

124
你可以添加这个运算符重载,这样它的工作就像没有绑定一样自然。
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: { lhs.wrappedValue = $0 }
    )
}

这将创建一个绑定,如果左侧操作数的值不为nil,则返回左侧的值,否则返回右侧的默认值。

在设置时,它只设置lhs的值,忽略与右侧有关的任何内容。

可以像这样使用:

TextField("", text: $test ?? "default value")

9
太聪明了!绝妙的解决方案!我不明白为什么SwiftUI没有原生支持这个... - Lupurus
2
你在哪里编写这个函数? - john doe
2
@johndoe 就像一个全局函数一样。它只是一个运算符重载。 - Jonathan.
1
为什么我会收到“??的无效重新声明”错误?我正在使用带有Swift 5的XCode 12.4。 - Travis Yang
1
这很棒,但它应该被表达为 Binding 中的静态函数。 - user652038
显示剩余4条评论

57

最终 API 无法实现此功能,但有一种非常简单和多用途的解决方法:

extension Optional where Wrapped == String {
    var _bound: String? {
        get {
            return self
        }
        set {
            self = newValue
        }
    }
    public var bound: String {
        get {
            return _bound ?? ""
        }
        set {
            _bound = newValue.isEmpty ? nil : newValue
        }
    }
}

这使得你可以保留可选项,同时使其与绑定兼容:

TextField($test.bound)

2
这太棒了,因为它完全有效!但我感觉一定有更好的方法,因为Binding有一个可选初始化器。只是我不知道如何在Swift简写中使用它。 - Johnston
这是一个更好的答案,因为它不那么冗长。 - Rammohan Raja
这似乎是一个很好的解决方案。有人尝试过将其与其他可选项一起使用,比如Bool?或Int?或枚举类型,比如Gender吗? - Zonker.in.Geneva
似乎不需要使用_bound。当用self替换时它可以工作。 - Maq

15

确实,目前在SwiftUI中TextField只能绑定到String变量,而不能绑定到String?。 但是你可以像这样定义自己的Binding

import SwiftUI

struct SOTest: View {
    @State var text: String?

    var textBinding: Binding<String> {
        Binding<String>(
            get: {
                return self.text ?? ""
        },
            set: { newString in
                self.text = newString
        })
    }

    var body: some View {
        TextField("Enter a string", text: textBinding)
    }
}
基本上,你将TextField的文本值绑定到这个新的Binding<String>绑定上,并且该绑定将其重定向到你的String? @State变量。

5
我更喜欢答案,这个答案由@Jonathon.提供,因为它简单、优雅,并在Optional.none(= nil)而不是.some时为编码器提供了一个原地基准情况。
不过,我认为在这里加入我的两分钱也是值得的。我从阅读Jim Dovey关于SwiftUI Bindings with Core Data的博客中学到了这个技巧。它本质上与@Jonathon提供的答案相同,但包括了一种可以复制到许多不同数据类型的好模式。
首先,在Binding上创建一个扩展。
public extension Binding where Value: Equatable {
    init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
        self.init(
            get: { source.wrappedValue ?? nilProxy },
            set: { newValue in
                if newValue == nilProxy { source.wrappedValue = nil }
                else { source.wrappedValue = newValue }
            }
        )
    }
}

然后在你的代码中这样使用…
TextField("", text: Binding($test, replacingNilWith: String()))

或者

TextField("", text: Binding($test, replacingNilWith: ""))

我最喜欢这个答案!在实际使用这个扩展程序时,更易于理解并提供了更清晰的方式,特别是在文本字段或其他地方。 - alexkaessner

1
你不想在每次加载页面时都创建一个新的Binding。而是应该使用自定义的ParseableFormatStyle
struct OptionalStringParseableFormatStyle: ParseableFormatStyle {

    var parseStrategy: Strategy = .init()

    func format(_ value: String?) -> String {
        value ?? ""
    }

    struct Strategy: ParseStrategy {

        func parse(_ value: String) throws -> String? {
            value
        }

    }

}

然后使用它
TextField("My Label", value: $myValue, format: OptionalStringParseableFormatStyle())

使其通用化

我更喜欢使用具有静态便利性的通用版本,可以适用于任何类型。

extension Optional {

    struct FormatStyle<Format: ParseableFormatStyle>: ParseableFormatStyle
    where Format.FormatOutput == String, Format.FormatInput == Wrapped {

        let formatter: Format
        let parseStrategy: Strategy<Format.Strategy>

        init(format: Format) {
            self.formatter = format
            self.parseStrategy = .init(strategy: format.parseStrategy)
        }

        func format(_ value: Format.FormatInput?) -> Format.FormatOutput {
            guard let value else { return "" }
            return formatter.format(value)
        }

        struct Strategy<OutputStrategy: ParseStrategy>: ParseStrategy where OutputStrategy.ParseInput == String {

            let strategy: OutputStrategy

            func parse(_ value: String) throws -> OutputStrategy.ParseOutput? {
                guard !value.isEmpty else { return nil }
                return try strategy.parse(value)
            }

        }

    }

}

extension ParseableFormatStyle where FormatOutput == String {

    var optional: Optional<FormatInput>.FormatStyle<Self> { .init(format: self) }

}

由于字符串在大多数情况下都具有格式样式,因此我创建了一个名为ParseableFormatStyle的标识。

extension String {

    struct FormatStyle: ParseableFormatStyle {

        var parseStrategy: Strategy = .init()

        func format(_ value: String) -> String {
            value
        }

        struct Strategy: ParseStrategy {

            func parse(_ value: String) throws -> String {
                value
            }

        }

    }

}

extension ParseableFormatStyle where Self == String.FormatStyle {

    static var string: Self { .init() }

}

extension ParseableFormatStyle where Self == Optional<String>.FormatStyle<String.FormatStyle> {

    static var optional: Self { .init(format: .string) }

}

现在你可以将其用于任何值。例如:
TextField("My Label", value: $myStringValue, format: .optional)
TextField("My Label", value: $myStringValue, format: .string.optional)
TextField("My Label", value: $myNumberValue, format: .number.optional)
TextField("My Label", value: $myDateValue, format: .dateTime.optional)

0

尝试使用可重用函数,这对我有用

@State private var name: String? = nil
private func optionalBinding<T>(val: Binding<T?>, defaultVal: T)-> Binding<T>{
    Binding<T>(
        get: {
            return val.wrappedValue ?? defaultVal
        },
        set: { newVal in
            val.wrappedValue = newVal
        }
    )
}
// Usage
TextField("", text: optionalBinding(val: $name, defaultVal: ""))

0

Swift 5.7,iOS 16

以下是我精选或编写的所有有用的Binding相关扩展。

这些内容为我覆盖了所有基础 - 我没有发现其他需要的。

希望对别人有所帮助。

import SwiftUI

/// Shortcut: Binding(get: .., set: ..) -> bind(.., ..)
func bind<T>(_ get: @escaping () -> (T), _ set: @escaping (T) -> () = {_ in}) -> Binding<T> {
    Binding(get: get, set: set)
}

/// Rebind a Binding<T?> as Binding<T> using a default value.
func bind<T>(_ boundOptional: Binding<Optional<T>>, `default`: T) -> Binding<T> {
    Binding(
        get: { boundOptional.wrappedValue ?? `default`},
        set: { boundOptional.wrappedValue = $0 }
    )
}

/// Example: bindConstant(false)
func bind<Wrapped>(constant: Wrapped) -> Binding<Wrapped> { Binding.constant(constant) }

extension Binding {
    
    /// `transform` receives new value before it's been set,
    /// returns updated new value (which is set)
    func willSet(_ transform: @escaping (Value) -> (Value)) -> Binding<Value> {
        Binding(get: { self.wrappedValue },
                set: { self.wrappedValue = transform($0) })
    }

    /// `notify` receives new value after it's been set
    func didSet(_ notify: @escaping (Value) -> ()) -> Binding<Value> {
        Binding(get: { self.wrappedValue },
                set: { self.wrappedValue = $0; notify($0) })
    }
}

/// Example: `TextField("", text: $test ?? "default value")`
/// See https://dev59.com/G1MI5IYBdhLWcg3wHHvM#61002589
func ??<T>(_ boundCollection: Binding<Optional<T>>, `default`: T) -> Binding<T> {
    bind(boundCollection, default: `default`)
}

// Allows use of optional binding where non-optional is expected.
// Example: `Text($myOptionalStringBinding)`
// From: https://dev59.com/G1MI5IYBdhLWcg3wHHvM#57041232
extension Optional where Wrapped == String {
    var _bound: String? {
        get {
            return self
        }
        set {
            self = newValue
        }
    }
    public var bound: String {
        get {
            return _bound ?? ""
        }
        set {
            _bound = newValue.isEmpty ? nil : newValue
        }
    }
}

/// Returns binding for given `keyPath` in given `root` object.
func keyBind<Root, Element>(_ root: Root, keyPath: WritableKeyPath<Root, Element>) -> Binding<Element> {
    var root: Root = root
    return Binding(get: { root[keyPath: keyPath] }, set: { root[keyPath: keyPath] = $0 })
}

/// Bind over a collection (is this inbuilt now? ForEach makes it available)
/// Override `get` and `set` for custom behaviour.
/// Example: `$myCollection.bind(index)`
extension MutableCollection where Index == Int {
    func bind(_ index: Index,
              or defaultValue: Element,
              get: @escaping (Element) -> Element = { $0 }, // (existing value)
              set: @escaping (Self, Index, Element, Element) -> Element = { $3 } // (items, index, old value, new value)
    ) -> Binding<Element> {
        var _self = self
        return Binding(
            get: { _self.indices.contains(index) ? get(_self[index]) : defaultValue },
            set: { if _self.indices.contains(index) { _self.safeset(index, set(_self, index, _self[index], $0)) } }
        )
    }
}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接