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.
"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()
}
}