0

In my case, I have a ContactProfile view which I want to update. I'd think it would be a good idea to make a copy of the Contact entity, edit the copy and then 'paste' the attributes of the copy back to the original entity and then save it in the managedObjectcontext.

The code I have so far only makes tempContact a reference to the original Contact. Which makes sense as the original contact is a class generated by CoreData, not a struct.

What is the best way to get around this? Or am I thinking too much into it? Is the @ObservedObject itself good enough for editing, since it is not saved to the data base until it is explicitly by calling a private func saveContext() function later in the code?

So far, I have this code:

    struct ContactProfile: View {
        
        @Environment(\.managedObjectContext) private var viewContext
        
        @FetchRequest(
            sortDescriptors: [NSSortDescriptor(keyPath: \Level.sortOrder, ascending: true)],
            animation: .default)
        
        private var levels: FetchedResults<Level>
        
        @ObservedObject var contact: Contact
        
/// Old code
///        @State private var tempContact: Contact

///        init(contact: Contact){
///            _contact = ObservedObject(initialValue: contact)
///            _tempContact = State(wrappedValue: contact)
///        }
   

///updated code below:
    
    @State private var tempContact = TempContact(firstName: "", lastName: "", birthDate: Date(), picture: nil)
        
    init(contact: ObservedObject<Contact>) {
        _contact = contact
        _tempContact = State(initialValue: tempContact)
        tempContact.firstName = contact.wrappedValue.firstName ?? ""
        tempContact.lastName = contact.wrappedValue.lastName ?? ""
    }

     ///end of updated code


        var body: some View {
            Form {
                Text("@Observed: \(contact.firstName ?? "Unknown") \(contact.lastName ?? "Unknown")")
                Text("@State: \(tempContact.firstName ?? "Unknown") \(tempContact.lastName ?? "Unknown")")
                
                TextField("Enter name", text: Binding(
                    get: { self.tempContact.firstName ?? ""},
                    set: { self.tempContact.firstName = $0 }
                )
                )
                
                [snipped the rest of the normal view code]
                

This ContactProfile is accessed from ContactsList like this:


struct ContactsList: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Contact.lastName, ascending: true)],
        animation: .default
    )
    
    private var contacts: FetchedResults<Contact>
    
    var body: some View {
        List {
            ForEach(contacts) { contact in
                NavigationLink(
                    destination: ContactProfile(contact: contact)) {
                    HStack (alignment: .firstTextBaseline) {
                        Text("\(contact.firstName ?? "Unknown") \(contact.lastName ?? "Unknown")")
                        Text("(\(contact.level?.name ?? ""))").font(.caption).foregroundColor(.gray)
                    }
                }
            }
            .onDelete(perform: deleteContacts)
        }
        .listStyle(PlainListStyle())
        .navigationTitle("Contacts")
        .navigationBarItems(trailing: Button(action: addContact) { Image(systemName: "plus") }
        )
    }
    
    [snip]
   
Rillieux
  • 587
  • 9
  • 23
  • What, exactly, are you trying to achieve by not using the CoreData model directly? That is actually pretty straightforward with no more gotchas than you would have running it through a struct. – Yrb Dec 21 '20 at 18:29
  • As I was writing, I wondered if this is needed or not. My goal is to let the user edit the properties without touching the `@ObservedObject`. And when the user is satisfied, then commit his choices (strings, UIImage example) to the `@ObservedObject`. But I noticed that the ObsevedObject while changing, wasn't updating the CoreData stack (the db, I don't know if stack is the right term) until the managedObjectContext was saved. So, is it fair to say that the ObservedObject is fulfilling that role of "draft" entity changes? The draft idea comes from the Stanford SwiftUI videos, lesson 11. – Rillieux Dec 21 '20 at 18:40
  • You can simply make the changes directly on the Core Data entities. I would check out [HackingWithSwift](https://www.hackingwithswift.com/books/ios-swiftui/creating-books-with-core-data) for a quick tutorial on it. – Yrb Dec 21 '20 at 18:48
  • But I don't want to make the changes directly on the entity until the user is done making those edits. Then save them all at once. – Rillieux Dec 21 '20 at 19:33
  • You can absolutely do that. You can save each edit, or save them up and do them when the user confirms the changes. The tutorial I sent you to goes through all of it. You have to keep going through the links, and not just look at the first page. Your way is just adding complexity for no gain. – Yrb Dec 21 '20 at 19:40
  • I actually have read and watched that tutorial. What I don't seem to find there is the "U" in CRUD. I can Create, Read and Delete, but I'm really trying to figure out Update. HackingWithSwift does not appear to go into that with their "books". the HWS detail view only Reads. I don't see where to Update there. This seems ot have some answers https://stackoverflow.com/questions/26345189/how-do-you-update-a-coredata-entry-that-has-already-been-saved-in-swift – Rillieux Dec 21 '20 at 19:55

1 Answers1

0

To handle the updates in Core Data is simple:

  1. Pass your CoreData managed object into the view.
  2. Set up variables to handle the data edit input and set them to the value of the CoreData object values, similarly to your struct, but as @State in the view.
  3. Once you have validated the input, set each attribute to it's corresponding @State variable like this: [Your Entity Name].setValue([Your @State Variable], forKey: "[Your Attribute Name]").
  4. Save your entity in the Managed Object Context, just like when you created that particular object.

This will update your object instead of creating a new one.

Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Yes, but HOW do I accomplish step 2? I set up a struct that has the same string properties as the entity as a @State var. But I cannot figure out how to init it. I get an error from the parent view "Cannot convert value of type 'FetchedResults.Element' (aka 'Contact') to expected argument type 'ObservedObject' – Rillieux Dec 21 '20 at 21:30
  • Unless you are dealing with a great number of variables, you can simply declare them and initialize them in an init(). Using the struct is not much more difficult in that you declare the struct and then access the parameters through . syntax. That really is not a Core Data question. That is a question as to how to pass data and initialize parameters. It isn't any different in concept once you are dealing with a managed object that has been initialized. – Yrb Dec 22 '20 at 01:19