Providing a default value for a SwiftUI Binding

Published on: November 15, 2022

Sometimes 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 type 'Binding'

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.

Categories

SwiftUI