Optional chaining the binding value in SwiftUI textfield

Viewed 389

I have the following model:

struct Food: Codable, Identifiable {
    var id = UUID()
    var name: String = ""
    var energy: Float?
    var water: Float?
    var macroProfile: MacronutrientProfile?
}

struct MacronutrientProfile: Codable {
    var carb: Float?
    var protein: Float?
    var fat: Float?
}

I am trying to bind values of an instance of this model to TextField as such:

struct FoodEditView: View {
    @State var food: Food
    var body: some View {
        Form {
            Section(header: Text("Basics").fontWeight(.bold)) {
                HStack {
                    Text("Name")
                    Spacer()
                    TextField("Name", text: $food.name)
                    .multilineTextAlignment(.trailing)
                }
                HStack {
                    Text("Energy")
                    Spacer()
                    TextField("Calories", value: $food.energy, formatter: calorie)
                    .multilineTextAlignment(.trailing)
                    .keyboardType(.numberPad)
                }
                HStack {
                    Text("Water")
                    Spacer()
                    TextField("Grams", value: $food.water, formatter: gram)
                    .multilineTextAlignment(.trailing)
                    .keyboardType(.numberPad)
                }
            }
            .textCase(.none)
            Section(header: Text("Macronutrients").fontWeight(.bold)) {
                HStack {
                    Text("Carbohydrates")
                    Spacer()
                    TextField("Grams", value: $food.macroProfile?.carb, formatter: gram)
                    .multilineTextAlignment(.trailing)
                    .keyboardType(.numberPad)
                }
                HStack {
                    Text("Protein")
                    Spacer()
                    TextField("Grams", value: $food.macroProfile?.protein, formatter: gram)
                    .multilineTextAlignment(.trailing)
                    .keyboardType(.numberPad)
                }
                HStack {
                    Text("Fat")
                    Spacer()
                    TextField("Grams", value: $food.macroProfile?.fat, formatter: gram)
                    .multilineTextAlignment(.trailing)
                    .keyboardType(.numberPad)
                }
            }
            .textCase(.none)

I am this rather strange, long list of errors when chaining macroProfile:

XCode Error

My question is, how is it that I am getting these errors when optional chaining macroProfile and not when using energy or water, both of which are also optional values? And what is the best approach to fix this?

1 Answers

When use the "dot chaining" syntax on a Binding its a shortcut syntax for creating a Binding. It doesn't have the same semantics "lookup a property of this thing" that the dot syntax usually holds.

So $food.name is not going to resolve the binding of food then reference one of its properties. It going to create a two-way Binding<String> to the name property of the food.

Similarly, when you have $food.macroProfile the value of that expression is a Binding<MacroNutrientProfile?>... a binding that will directly change a value in food (and the value it can change is an optional). It is not a resolution of the binding $food followed by referencing one of that object's properties.

$food.macroProfile?.carb is nonsensical because $food.macroProfile is of type Binding<MacroNutrientProfile?> which is not an optional type. So you see errors.

$food.name is not nonsensical because it is a Binding<String> and you're not trying to treat a non-optional value as an optional.

One way to change it is to use a custom binding:

struct Food: Codable, Identifiable {
    var id = UUID()
    var name: String = ""
    var energy: Float?
    var water: Float?
    var macroProfile: MacronutrientProfile?
}

struct MacronutrientProfile: Codable {
    var carb: Float?
    var protein: Float?
    var fat: Float?
}

struct SomeView : View {
    @State var food: Food
    let gram = NumberFormatter()

    var body : some View {
        let carbBinding = Binding<Float?>(get: { food.macroProfile?.carb },
                                          set: { newValue in food.macroProfile?.carb = newValue })

        return HStack {
           Text("Carbohydrates")
           Spacer()
            TextField("Grams", value: carbBinding, formatter: gram)
           .multilineTextAlignment(.trailing)
           .keyboardType(.numberPad)
       }
    }
}
Related