Reputation: 3398
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
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
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())
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
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
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
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
Reputation: 164
@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
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
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
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