Providing a default value for a SwiftUI Binding
Published on: November 15, 2022Sometimes in SwiftUI apps I’ll find that I have a model with an optional value that I’d like to pass to a view that requires a non optional value. This is especially the case when you’re using Core Data in your SwiftUI apps and use auto-generated models.
Consider the following example:
class SearchService: ObservableObject {
@Published var results: [SearchResult] = []
@Published var query: String?
}
Let me start by acknowledging that yes, this object can be written with a query: String = ""
instead of an optional String?
. Unfortunately, we don’t always own or control the models and objects that we’re working with. In these situations we might be dealing with optionals where we’d rather have our values be non-optional. Again, this can be especially true when using generated code (like when you’re using Core Data).
Now let’s consider using the model above in the following view:
struct MyView: View {
@ObservedObject var searchService: SearchService
var body: some View {
TextField("Query", text: $searchService.query)
}
}
This code will not compile because we need to pass a binding to a non optional string to our text field. The compiler will show the following error:
Cannot convert value of type
Binding<String?>
to expected argument typeBinding<String>
One of the ways to fix this is to provide a custom instance of Binding
that can provide a default value in case query
is nil
. Making it a Binding<String>
instead of Binding<String?>
.
Defining a custom binding
A SwiftUI Binding
instance is nothing more than a get
and set
closure that are called whenever somebody tries to read the current value of a Binding
or when we assign a new value to it.
Here’s how we can create a custom binding:
Binding(get: {
return "Hello, world"
}, set: { _ in
// we can update some external or captured state here
})
The example above essentially recreates Binding
's .constant
which is a binding that will always provide the same pre-determined value.
If we were to write a custom Binding
that allows us to use $searchService.query
to drive our TextField
it would look a bit like this:
struct MyView: View {
@ObservedObject var searchService: SearchService
var customBinding: Binding<String> {
return Binding(get: {
return searchService.query ?? ""
}, set: { newValue in
searchService.query = newValue
})
}
var body: some View {
TextField("Query", text: customBinding)
}
}
This compiles, and it works well, but if we have several occurrences of this situation in our codebase, it would be nice if had a better way of writing this. For example, it would neat if we could write the following code:
struct MyView: View {
@ObservedObject var searchService: SearchService
var body: some View {
TextField("Query", text: $searchService.query.withDefault(""))
}
}
We can achieve this by adding an extension on Binding
with a method that’s available on existing bindings to optional values:
extension Binding {
func withDefault<T>(_ defaultValue: T) -> Binding<T> where Value == Optional<T> {
return Binding<T>(get: {
self.wrappedValue ?? defaultValue
}, set: { newValue in
self.wrappedValue = newValue
})
}
}
The withDefault(_:)
function we wrote here can be called on Binding
instances and in essence it does the exact same thing as the original Binding
already did. It reads and writes the original binding’s wrappedValue
. However, if the source Binding
has nil
value, we provide our default.
What’s nice is that we can now create bindings to optional values with a pretty straightforward API, and we can use it for any kind of optional data.