1

So I've got an entity in Core Data, lets call it Parent defined as:

extension Parent {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Parent> {
        return NSFetchRequest<Timer>(entityName: "Parent")
    }

    @NSManaged public var id: UUID
    @NSManaged public var children: Children
}

And it has some children

public class Children: NSObject, NSCoding, ObservableObject {
    @Published var children: [Child]

    init(children: [Child] = [Child]()) {
        self.cycles = cycles
    }

    // code for encoding/decoding ...
}

and child has some definition of a collection of UUID/Strings/Int and is a struct

So initially the Parents are displayed in a List View To insert a new parent you tap a plus button, new Parent is created and is persistent

There is another view to add children to the parent

So what's the problem? Whenever a new parent is created, it makes the context dirty and context.hasChanges() returns true and the context can be saved for persistence

Whenever the children are updated the context is not made dirty and context.hasChanges() returns false, therefore the updates are not saved

I think the problem is because of classes being reference types it does not look like Parent has changed as the children object is the same - but the data in it has changed. So how do I get the changes to save?

Short of deleting the Parent and remaking it when changes occur or updating a last updated variable in the Parent I'm not seeing how I get the context to realise there are changes

Ilyas
  • 38
  • 7
  • Is there a reason you aren’t using a one to many relationship to manage your children? I’m assuming the children property is a Tranformable property hence the coding. – Warren Burton Oct 10 '20 at 09:44
  • Because I don't know what I'm doing I'll investigate... thanks for the direction – Ilyas Oct 10 '20 at 10:41

1 Answers1

1

tldr: Don't use transformable properties to manage relationships in CoreData , use actual relationships.

Your CoreData isn't being marked "dirty" because your children property is essentially a blob of Data that encodes/decodes on the fly. There are no change notifications generated when you update your children.

While you could fix the current situation with an implementation like this:

extension Parent {

    func addChild(_ child: Child) {
        willChangeValue(forKey: #keyPath(children))
        let childrenContainer = children as? Children ?? Children()
        childrenContainer.children.append(child)
        self.children = childrenContainer
        didChangeValue(forKey: #keyPath(children))
    }

}

Generally you wouldn't model a parent/child relationship like this in CoreData as its about 6-10x slower than a real relationship and its not how to use what CoreData does.

What you probably want is a one-to-many relationship

enter image description here

Which will manifest in your code as

@NSManaged public var children: NSSet?

You can also use an NSOrderedSet (observe the ordered tick box below the relationship type ).

How you use the Delete Rule will depend on how you wish to model your data. Nullify will leave orphan children behind on Parent delete, while Cascade will delete any children on Parent delete.

Warren Burton
  • 17,451
  • 3
  • 53
  • 73
  • It works! Thank you. One thing to note is NSOrderedSet doesn't appear to work with Cloudkit (https://stackoverflow.com/questions/56967051/how-to-set-an-ordered-relationship-with-nspersistentcloudkitcontainer) – Ilyas Oct 11 '20 at 20:18
  • One thing I didn't get working was when creating a new object from the entity was initializing variables, for example an id: UUID. If I create a custom initalizer for it it will overwrite the stored one when fetching - for now I set the field when I make a new object but it seems incorrect – Ilyas Oct 11 '20 at 20:20
  • 1
    Glad it worked. Not quite sure what you mean with the UUID but you might like to look at `awakeFromInsert` and `awakeFromFetch` on NSManagedObject for lifecycle options. – Warren Burton Oct 11 '20 at 20:30