Optional extension with custom binding in SwiftUI

Viewed 1362

Is there a way to create an extension for Optional value so that we don't need to create custom binding individually.

class ViewModel: ObservableObject {
    @Published var text: String?
}

struct ContentView: View {
    @ObservedObject var model: ViewModel
    
    var body: some View {
        let binding = Binding(
            get: { self.model.text ?? "" },
            set: { self.model.text = $0 }
        )
        
        return VStack {
            TextField("Text", text: binding)
        }
    }
}

I'm thinking about doing something like this

extension Optional where Wrapped == String {
    func optionalBinding() -> Binding<String> {
        return Binding(get: {
            self ?? ""
        }, set: { newValue in
            self = newValue
        })
    }
}

Any suggestions on this?

3 Answers

You may try extending Binding instead of Optional:

extension Binding where Value == String? {
    var optionalBinding: Binding<String> {
        .init(
            get: {
                self.wrappedValue ?? ""
            }, set: {
                self.wrappedValue = $0
            }
        )
    }
}

And use it like this:

class ViewModel: ObservableObject {
    @Published var text: String?
}

struct ContentView: View {
    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            TextField("Text", text: $model.text.optionalBinding)
            Text(String(describing: model.text))
        }
    }
}

Nil-coalescing operator.

public extension Binding {
  static func ?? <Wrapped>(optional: Self, defaultValue: Wrapped) -> Binding<Wrapped>
  where Value == Wrapped? {
    .init(
      get: { optional.wrappedValue ?? defaultValue },
      set: { optional.wrappedValue = $0 }
    )
  }
}
var wrappedValue: String? = ""
@Binding(
  get: { wrappedValue },
  set: { wrappedValue = $0 }
) var string;

let stringOrDefault = $string ?? ""
XCTAssertEqual(stringOrDefault.wrappedValue, "")
string = nil
XCTAssertEqual(stringOrDefault.wrappedValue, "")
stringOrDefault.wrappedValue = ""
XCTAssertEqual(string, "")

To build on @pawello2222's answer, you can use a more generic extension catching all values:

extension Binding {
    func unwrapped<T>(_ defaultValue: T) -> Binding<T> where Value == Optional<T> {
        let binding = Binding<T>(get: { self.wrappedValue ?? defaultValue }, set: { self.wrappedValue = $0 })
        return binding
    }
}

And it is used like:

class ViewModel: ObservableObject {
    @Published var text: String?
}

struct ContentView: View {
    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            TextField("Text", text: $model.text.unwrapped(String()))
            Text(String(describing: model.text))
        }
    }
}
Related