core-dataswiftuinsobject

Is it possible to save a value to CoreData using key path?


In a parent view, I have this:

LongPressEditableText(contents: "\(workout.name ?? "")", context: workout, keyPath: \WorkoutEntity.name)

referencing a string field of a WorkoutEntity in CoreData.

The LongPressEditableText is to be a component which is usually just a Text(), but when long pressed, becomes a TextField with the same contents, editable. On submit it should update the UI (it does this fine), but it should also save the new value to the appropriate spot in CoreData.

struct LongPressEditableText: View {
    
    @State var contents: String
    
    @Environment(\.managedObjectContext) private var viewContext
    
    var context: NSObject
    var keyPath: KeyPath<NSObject, String?>
    
    @State var inEditMode: Bool = false
    
    var body: some View {
        if inEditMode {
            TextField("test", text: $contents)
                .onSubmit {
                    context[keyPath: keyPath] = contents
                    do {
                        try viewContext.save()
                    } catch {
                        let nsError = error as NSError
                        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                    }
                    
                    inEditMode.toggle()
                    
                }
        } else {
            Text(contents)
                .onLongPressGesture {
                    inEditMode.toggle()
                }
        }
    }
}

At the moment, I get two errors. In my parent view Cannot convert value of type 'KeyPath<WorkoutEntity, String?>' to expected argument type 'KeyPath<NSObject, String?>' and in the LongPressEditableText view Cannot assign through subscript: key path is read-only

I can solve the first by forcing KeyPath but that's not a solution as I want the editable field to work with a number of different entities with string fields, so I'd like it to be generic. The second I am stumped about, this is as close as I've been able to get to success.


Solution

  • "Generics isn’t my primary concern...", yes it is because it is a very helpful solution here that tells the compiler and runtime what type of object is used in the text field.

    First of all since this is Core Data we shouldn't use NSObject but instead NSManagedObject so lets make the view generic with a type that inherits from NSManagedObject and then use the generic type inside for the properties.

    struct LongPressEditableText<ManagedObject: NSManagedObject>: View {
        @Environment(\.managedObjectContext) private var viewContext
        @State private var contents: String = ""
    
        @State var object: ManagedObject
        var keyPath: ReferenceWritableKeyPath <ManagedObject, String?>
    

    Notice that the property object (context in your code) is declared to be of the generic type and that the keyPath is also defined to hold the same type. I have also changed from KeyPath to ReferenceWritableKeyPath since the generic type is a class and we want to use the key path to update the object.

    And to use the field here is an example, since the view is generic the compiler can deduct that the generic type is Item and also check that it has a property text

    struct DetailView: View {
        @ObservedObject var item: Item
        var body: some View {
            VStack {
                LongPressEditableText(object: item, keyPath: \.text)
            }
            .padding()
        }
    }