Brandon Bradley
Brandon Bradley

Reputation: 3398

SwiftUI Optional TextField

Can SwiftUI Text Fields work with optional Bindings? Currently this code:

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

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

produces the following error:

Cannot convert value of type 'Binding< String?>' to expected argument type 'Binding< String>'

Is there any way around this? Using Optionals in data models is a very common pattern - in fact it's the default in Core Data so it seems strange that SwiftUI wouldn't support them

Upvotes: 75

Views: 26618

Answers (9)

Nathan
Nathan

Reputation: 11

As the previous answers are giving Sendable errors in Swift 6, here an updated answer:

public extension Binding where Value: Sendable, Value: Equatable {
    static func ??(lhs: Binding<Optional<Value>>, rhs: Value) -> Binding<Value> {
        Binding {
            lhs.wrappedValue ?? rhs
        } set: {
            lhs.wrappedValue = $0 == rhs ? nil : $0
        }
    }
}

This is a modified version of the previous answer of lar3ry that conforms to Swift 6. It will also set the value to nil if the value is the same as the default value.

Usage: TextField("Label", text: $var ?? "")

Upvotes: 1

Justin Oroz
Justin Oroz

Reputation: 659

You don't want to be creating a new Binding every body load. Use a custom ParseableFormatStyle instead.

struct OptionalStringParseableFormatStyle: ParseableFormatStyle {

    var parseStrategy: Strategy = .init()

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

    struct Strategy: ParseStrategy {

        func parse(_ value: String) throws -> String? {
            guard !value.isEmpty else {
                return nil  // How to handle empty/nil is up to the developer.
            }
            return value
        }

    }

}

Then use it

TextField("My Label", value: $myValue, format: OptionalStringParseableFormatStyle())

Make It Generic

I prefer to make generic versions of these styles with static conveniences that can work with any type.

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) }

}

String does not have a format style as it is redundant in most cases so I make an identity 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) }

}

Now you can use this for any value. Examples:

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)

Upvotes: 4

lar3ry
lar3ry

Reputation: 580

This is just a slightly cleaner version of Jonathan.'s answer, rewritten as a static function for Binding:

public extension Binding {
    /// Create a non-optional version of an optional `Binding` with a default value
    /// - Parameters:
    ///   - lhs: The original `Binding<Value?>` (binding to an optional value)
    ///   - rhs: The default value if the original `wrappedValue` is `nil`
    /// - Returns: The `Binding<Value>` (where `Value` is non-optional)
    static func ??(lhs: Binding<Optional<Value>>, rhs: Value) -> Binding<Value> {
        Binding {
            lhs.wrappedValue ?? rhs
        } set: {
            lhs.wrappedValue = $0
        }
    }
}

Here's an example of it in action:

struct TestView: View {
    @State
    var optionalText: String? = nil

    var body: some View {
        VStack {
            TextField("Type HERE:", text: $optionalText ?? "default value")

            Button {
                print(optionalText)
            } label: {
                Text("Tap Me!")
            }
        }
    }
}

Note: The optionalText variable will contain nil if the user does not modify the text field. If any modifications are made, a non-nil value will be placed into that variable, although the variable's type is still optional.

Upvotes: 7

kwiknik
kwiknik

Reputation: 885

Swift 5.7, iOS 16

Here are all the useful Binding related extensions I've curated or written.

These cover all bases for me - I haven't found any others to be needed.

I hope someone finds them useful.

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://stackoverflow.com/a/61002589/5970728
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://stackoverflow.com/a/57041232/5970728
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)) } }
        )
    }
}

Upvotes: 0

andrewbuilder
andrewbuilder

Reputation: 3799

I prefer the answer provided by @Jonathon. as it is simple and elegant and provides the coder with an insitu base case when the Optional is .none (= nil) and not .some.

However I feel it is worth adding in my two cents here. I learned this technique from reading Jim Dovey's blog on SwiftUI Bindings with Core Data. Its essentially the same answer provided by @Jonathon. but does include a nice pattern that can be replicated for a number of different data types.

First create an extension on 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 }
            }
        )
    }
}

Then use in your code like this...

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

or

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

Upvotes: 6

Tun Cham Roeun
Tun Cham Roeun

Reputation: 164

Try this works for me with reusable function

@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: ""))

Upvotes: 0

Jonathan.
Jonathan.

Reputation: 55594

You can add this operator overload, then it works as naturally as if it wasn't a Binding.

func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
    Binding(
        get: { lhs.wrappedValue ?? rhs },
        set: { lhs.wrappedValue = $0 }
    )
}

This creates a Binding that returns the left side of the operator's value if it's not nil, otherwise it returns the default value from the right side.

When setting it only sets lhs value, and ignores anything to do with the right hand side.

It can be used like this:

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

Upvotes: 142

Frederic Adda
Frederic Adda

Reputation: 6092

True, at the moment TextField in SwiftUI can only be bound to String variables, not String?. But you can always define your own Binding like so:

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)
    }
}

Basically, you bind the TextField text value to this new Binding<String> binding, and the binding redirects it to your String? @State variable.

Upvotes: 17

Brandon Bradley
Brandon Bradley

Reputation: 3398

Ultimately the API doesn't allow this - but there is a very simple and versatile workaround:

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
        }
    }
}

This allows you to keep the optional while making it compatible with Bindings:

TextField($test.bound)

Upvotes: 57

Related Questions