64

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

Brandon Bradley
  • 3,160
  • 4
  • 22
  • 39
  • TextField requires a Binding, not a Binding. So it seems you cannot. Could you elaborate why you would need such binding? – kontiki Jul 13 '19 at 18:37
  • I have a Contact NSManaged Object with the property `twitter: String?` that allows the user to optionally associate a Twitter account with the contact using a TextField – Brandon Bradley Jul 13 '19 at 18:45
  • 2
    Could you use a null string instead? `@State var test = ""`? If not, then what do you want with the `TextField` when your string is `nil`? –  Jul 13 '19 at 20:41
  • 4
    Yes - the problem is Core Data which creates NSManaged Strings as Optionals – Brandon Bradley Jul 13 '19 at 21:03
  • Well, with `String?` you can argue that `nil` and `""` are "equivalent", but what if your data model contains `CLLocation?` - what is the "equivalent" of `nil`? SwiftUI does not work well with `Optional`, generally. – Grimxn Dec 20 '19 at 13:17
  • 4
    Filed a Feedback Assistant report requesting optional bindings, FB7619680. – Curiosity Apr 05 '20 at 07:15
  • 1
    Tick Default String for the attribute in the model – malhal Jun 01 '20 at 21:12

6 Answers6

120

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")
Jonathan.
  • 53,997
  • 54
  • 186
  • 290
  • 9
    This is absolutely genius!!! Fantastic solution! I don't understand, why this is not supported by SwiftUI out of the box... – Lupurus Apr 15 '20 at 07:12
  • 2
    Where do you write this function? – john doe May 30 '20 at 19:19
  • 2
    @johndoe just as a global function. It's just an operator overload – Jonathan. Jun 01 '20 at 14:04
  • @johndoe you create a new swift file with he first line "import SwiftUI" then below that paste his code. – ngb Apr 10 '21 at 05:13
  • 1
    How come I get a "Invalid redeclaration of '??'" error? I'm using XCode 12.4 with Swift 5. – Travis Yang Jul 12 '21 at 03:00
  • I just want to reiterate that this is an amazingly simple and effective solution. Thank you! – Kemo Sabe Sep 01 '21 at 19:59
  • This is great but it should be expressed as a static function within `Binding`. –  Feb 06 '23 at 20:25
  • 1
    @Jessy Why? I'm not sure there's any difference? – Jonathan. Feb 07 '23 at 20:10
  • @Jonathan Global operators are Swift 1.x style—functionally equivalent but messy practice. you don't need some archaic "`T`" when `Value` is already defined. `static func ?? (optional: Binding, default: Value) -> Self {` –  Feb 07 '23 at 20:55
57

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)
Brandon Bradley
  • 3,160
  • 4
  • 22
  • 39
  • 2
    This is great as it totally works! I feel like there has to be a better way though because `Binding` has an [`optional initializer`](https://developer.apple.com/documentation/swiftui/binding/3115381-init). I just don't know how to use it with the Swift shorthand. – Johnston Feb 12 '20 at 13:12
  • This is a better answer as it is less verbose. – Rammohan Raja Sep 10 '21 at 15:31
  • This seems like a great solution. Has anyone tried it with other Optionals, like Bool? or Int? or with an enum, like Gender? – Zonker.in.Geneva Jan 20 '22 at 19:02
  • It seems like _bound is not needed. It works when replaced with self. – Maq Sep 28 '22 at 16:24
14

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.

Frederic Adda
  • 5,905
  • 4
  • 56
  • 71
5

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: ""))
andrewbuilder
  • 3,629
  • 2
  • 24
  • 46
  • I like this answer the most! It’s better to understand and gives a more clear way when I’m actually using this extension in a TextField or something else. – alexkaessner Jun 16 '23 at 08:18
0

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

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

kwiknik
  • 570
  • 3
  • 7