Search code examples
swiftswiftuiswiftdata

How to clear a TextField when it bound to a SwiftData object that isn't optional


Say you have a SwiftData object that doesn't allow optionals:

@Model
class MySet: Identifiable {
    var id: UUID
    var weight: Int
    var reps: Int
    var isCompleted: Bool
    var exercise: Exercise

Then you get from your MySet data a list of these MySets and append them into a State:

  @State var sets: [MySet]

            if let newSets = exercise.sets {    //unwrapping the passed exercises sets
                if (!newSets.isEmpty) {     //we passed actual sets in so set them to our set
                    sets = newSets
                }

And you use that State list in a ForEach and bind the Textfield directly to the data like so:

 ForEach($sets){ $set  in

 TextField("\(set.weight)", value: $set.weight, formatter: NumberFormatter())
                                        .keyboardType(.decimalPad)

   TextField("\(set.reps)", value: $set.reps,formatter: NumberFormatter())
                                        .keyboardType(.decimalPad)
                                    


}

This will bind whatever is written into the TextField directly to the SwiftData object which is a problem because say you have 50 written and you want to change it to 60 you need to be able to clear the 50 to write the 60 and since the SwiftData object doesn't allow nil it will allow you to remove the 0 but not the 5. Is there anyway to remedy this without having the swiftData object allow optionals for example catching the clearing of the TextField and making it 0 instead of nil when it's cleared?

I attempt to fix it by binding TextField to a new State and using onChanged:

@State private var reps: Int = 0
@State private var weight: Int = 0

                            TextField("\(myset.weight)", value: $weight, formatter: NumberFormatter())
                                .keyboardType(.decimalPad)
                             
                                .onChange(of: weight) { newWeight in
                                    // Code to run when propertyToWatch changes to newValue
                                    if newWeight != 0 {
                                        myset.weight = newWeight
                                    }
                                }

but now it is changing every single reps and weight of all in sets when I type into one instead of just the one I am typing on


Solution

  • You could create a custom binding which should do the trick:

    private func weightBinding(for set: MySet) -> Binding<Int?> {
        Binding<Int?>(
             get: { set.weight },
             set: { value in
                 if let value {
                    set.weight = value
                 }
              }
          )
    }
    
    ForEach(sets) { set in
       TextField("\(set.weight)", value: weightBinding(for: set), formatter: NumberFormatter())
    }
    
    // or as extension if you prefer:
    extension MySet {
        var weightBinding: Binding<Int?> {
            Binding<Int?>(
                get: { self.weight },
                set: { value in
                    if let value {
                        self.weight = value
                    }
                }
            )
        }
    }
    
    TextField("\(set.weight)", value: set.weightBinding, formatter: NumberFormatter())
    
    

    Like this you can clear the Textfield but if you don't enter a new value and stop editing it uses the last value.

    EDIT:

    If you want to set the value to 0 when the textfield is empty you need to change the Binding to this:

    
     Binding<Int?>(
             get: { set.weight },
             set: { value in
                 self.weight = value ?? 0
              }
          )