Promoting Binding<Value?> to Binding<Value>

I'm wondering how to best "promote" a Binding<Value?> to a Binding<Value> for cases where a model property is optional (e.g. a Core Data generated class) but the bound view does not accept optional values. Here's an example use case:

import SwiftUI

class Model: ObservableObject {
    @Published var title: String?
}

struct EditModelView: View {
    @ObservedObject var model: Model

    var body: some View {
        VStack {
            Text(model.title ?? "nil")
            TextField("Title", text: $model.title) // Doesn't compile: Cannot convert value of type 'Binding<String?>' to expected argument type 'Binding<String>'
        }
    }
}

I was thinking about something like this (limited to Strings):

extension Binding where Value == String? {
    func onNone(_ fallback: String) -> Binding<String> {
        return Binding<String>(get: {
            return self.wrappedValue ?? fallback
        }) { value in
            self.wrappedValue = value
        }
    }
}

And usage would be:

TextField("Title", text: $model.title.onNone(""))

This seems to work the way I want (provides the default value when the original Binding's wrapped value is nil) but I'm wondering if there are potential gotchas with chaining Bindings in this way.

Questions:

  1. Does something like this already exist in SwiftUI?
  2. Is this a good or bad idea?
  3. Are there any concerns about how the original Binding's Transaction is not used? It's unclear to me how it would be applied.
  4. Is there a way to write this in a more generalized way? e.g. something along the lines of:
extension Binding where Value == Optional<Wrapped> {
    func onNone(_ fallback: Wrapped) -> Binding<Wrapped> {
        return Binding<Wrapped>(get: {
            return self.wrappedValue ?? fallback
        }) { value in
            self.wrappedValue = value
        }
    }
}
3 Likes

IMO it's only useful when

  1. Upstream API uses Optional and you can't change that
  2. The fallback is never a valid value.

Because otherwise I'd really like convert them to non-optional.

IIUC, you shouldn't need to worry about Transaction unless you take it directly as an argument.

You can use operator

func ??<T>(binding: Binding<T?>, fallback: T) -> Binding<T> {
  return Binding(get: {
    binding.wrappedValue ?? fallback
  }, set: {
    binding.wrappedValue = $0
  })
}

You can use a different operator if ?? is stressing your compiler.

4 Likes

Another option (actually by PointFree) is to turn the Binding<T?> into a Binding<T>?

Like...

extension Binding {
    func unwrap<Wrapped>() -> Binding<Wrapped>? where Optional<Wrapped> == Value {
        guard let value = self.wrappedValue else { return nil }
        return Binding<Wrapped>(
            get: {
                return value
            }, 
            set: { value in
                self.wrappedValue = value
            }
        )
    }
}

This way you can use your binding like...

self.$myBinding.unwrap().map { value in
    // do stuff
}

Now if your binding contains nil then your closure isn't run but if the binding contains a value you can get the value out of it without having a fallback.

1 Like

It should be

func unwrap<Wrapped>() -> Binding<Wrapped>?
  where Optional<Wrapped> == Value {

Though I never quite get the utility of this. It does nothing when you set a value to a nil storage.

1 Like

I think the main motivation behind it from PointFree was that SwiftUI will deal with it by not placing a view if the wrapped value is null.

Also, there was something about SwiftUI not being able to use bindings of optional values.

It was very heavily SwiftUI focussed so understandable not to use it if you're not also using SwiftUI.

2 Likes

It's less about not using Binding of optional, and more about not being able to easily cast in to and out of optional value, which is understandable since Binding<X> and Binding<X?> are rightfully invariant. So there's some friction when the supplied Binding doesn't match the optionality of the demanded Binding.

What I don't understand is that this kind of extension just hides that friction and will silently ignore mistreated data. It also cut ties to the original value in the getter, but write back to the original value in the setter. I thought it was a bug, but it is so prevalent that I think there's something more to it.

I'd make more sense if the setter just throws error or ignore the value, but that's not what I've been seeing.

I mean, I haven't been using this kind of thing with anything but SwiftUI, so I hope I qualify.

1 Like

I'm jealous that you get to use SwiftUI so much. I'm still learning :smiley: only UIKit at work so haven't had the opportunity to fully get my hands on it yet.

The unwrap() operator posted above is most useful when you have an optional value in your state that determines if some UI should be visible/hidden, and when visible you want a Binding<Value> to hand to it, not a Binding<Value?>. This is similar to how alerts, sheets and navigation work too, where an optional piece of state controls whether something is shown or dismissed.

It may sometimes make sense to just coalesce to a fallback, like in the case of a Binding<String?> and "", but I think in general that causes you to make choices that you were specifically trying to avoid by using an optional in the first place. And many times a fallback is probably not appropriate, such as if you had a Binding<User?>.

For the times a fallback is appropriate, it would probably be better to just use a non-optional type in state.

3 Likes

I figured that the use cases would be ones that invalidate the old data if/when the source mutates. So isVisible/isHidden is probably a good example.

Still, returning Binding is very close to abusing the Binding API since value does not follow data source, and any source mutation invalidates the returned instance, meaning the setter can be meaningfully called only once. If we're really quoting alert and sheet, it'd be more appropriate to return (result: Value, dismiss: () -> ()) especially that the only value you can reasonably set is nil. Understandably, it's a relatively big luggage to carry around, so there's probably some thin line being thread here. Using a generic name like unwrap is giving it too much credit on its versatility, though. Perhaps it's fine for the use case you mentioned, but it relies on a lot of conventions on top of the Binding contract.

Don't get me wrong, I'm not advocate to fallback use case. The friction on type mismatch is usually very subtle, and should be handle on case-by-case basis. The fallback solution, for one, doesn't let the underlying framework know that the current value is a placeholder, that could/should be rendered differently.

I don't think this is true for examples I'm thinking of. We can take an example straight from Apple's code samples. In the "Working with UI Controls" sample there is a section ("Delay Edit Propagation") that details how to put a form in "edit" mode so that you can make edits, which you can either save or cancel out of.

If we were to approach this in a declarative way I think we'd want to hold onto some optional draft state in our view:

struct ProfileHost: View {
  @State var draftProfile: Profile?
  @EnvironmentObject var currentProfile: Profile
}

So that when it's non-nil (meaning the draft is active) we show a view that represents an editable form of the profile data, and when it's nil (the draft is not active) we show the current profile info in a non-editable style:

if let draft = self.$draftProfile.unwrap() {
  ProfileEditor(profile: draft)
} else {
  ProfileSummary(profile: self.currentProfile)
}

That is very declarative to me because we are allowing the optionality of the Profile state to dictate what view is shown and hidden, and the ProfileEditor naturally gets its binding from the state.

And the draft obtained from the unwrap() operation is definitely connected to the original self.$draftProfile binding. Any mutations made to it in the child view will be instantly seen in the parent.

However, this is not possible with SwiftUI right now, so instead Apple suggests holding onto some additional state (in this case its an editMode, but might as well be a boolean):

struct ProfileHost: View {
  @Environment(\.editMode) var mode
  @State var draftProfile = Profile.default
  @EnvironmentObject var currentProfile: Profile
}

There's a few things not right about this domain modeling. First we have a boolean value and a profile value. That's more data than is necessary. Also, since we can't use an optional Profile? we are forced to provide a fallback value of User.default, even though that value should never be displayed. It should only ever start the current profile. This is a strange choice we are being forced to make, and we shouldn't have to.

And then that domain modeling leads one to the following kind of view hierarchy:

if self.mode?.wrappedValue == .active {
  ProfileEditor(profile: self.$draftProfile)
} else {
  ProfileSummary(profile: self.currentProfile)
}

This is not as declarative to me because we need some secondary data to determine which view is displayed, and then use completely unrelated data to pass down to the child view.

The code sample I shared about using unwrap() shows the similarities with the alert, sheet and navigation APIs. It is allowing optional state to drive the presence and dismissal of a child view. It is not true that the only reasonable thing to do is dismiss. We can also make mutations to the binding while it is present.

Yeah absolutely, and I think that if you were to try to improve the fallback solution to allow for that placeholder logic you would invariably be led back to an operator that looks like unwrap.

2 Likes

I'm trying to say that every mutation needs to replace the Binding instance, which is a very subtle requirement. Well, I suppose if enough people follow convention, it becomes trivial...

In all seriousness, there's no functional difference between unwrap returning Binding vs (Value, dismiss: (Value?) -> ()) (or (Value, dismiss: () -> ()) on pure dismiss case, for that matter). You can easily convert between the two forms, including your code above. It's just a preference at this point that I think the latter better signals that it is single-use.

Case like this should be minimized anyway. Binding<Value?> and Binding<Value>? aren't super related.

I'll to be a little pedantic. If the data is read-only, like label, we'd probably just unwrap it to Value?. If the data is read-write, like text field, Binding<Value?> is probably already correct (And if you mean to enable/disable with placeholder, that's just the alert case).

For anyone still looking at this. This is how I convert Binding<Value?> to Binding

extension Binding {
    func withDefault<Wrapped>(value defaultValue: Wrapped) -> Binding<Wrapped> where Optional<Wrapped> == Value {
        Binding<Wrapped> {
            wrappedValue ?? defaultValue
        } set: { newValue in
            self.wrappedValue = newValue
        }
    }
}

There’s a built in initialiser that does this already btw.

That's not the same. The builtin initialiser turns
Binding<Optional<Value>>Optional<Binding<Value>>

The code provided by OP turns
Binding<Optional<Value>>Binding<Value>

The function signature in Fogmeister's post is: func unwrap<Wrapped>() -> Binding<Wrapped>?
That returns an optional binding which is the same as the init

1 Like

You're right. I misread what comment you were replying to. In that case, the builtin init should do exactly the same thing :-)

I would be very wary of using this initializer. It has had bugs that can cause crashes since day one (reported FB8367784 in August 2020), and while more recent versions of iOS no longer crash on simple usage, there are still crashes out there on iOS 17 (and I updated FB8367784 with an example just last week).

Instead, I'd recommend defining your own version that doesn't crash. We also ship one in a library here.

2 Likes