2

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?

dsynkd
  • 1,999
  • 4
  • 26
  • 40

1 Answers1

5

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)
       }
    }
}
Scott Thompson
  • 22,629
  • 4
  • 32
  • 34