How to Set an Ordered Relationship with Nspersistentcloudkitcontainer

How to set an ordered relationship with NSPersistentCloudKitContainer?

(applies: as of iOS 13, and early betas of 14)

Cloud kit can't use ordered relationships (which just blows, as ordered data is a fundamental Thing That Exists). The reason is that everything is backed with CKRecord (cloud kit records) not actual core data stuff -- it's a completely different type of data storage, and the PersistentCloudKitContainer is doing on-the-fly rewrites of your data to CKRecords and back. CKRecords don't have mechanisms that work for maintaining journals of ordered items the way we need in Core Data ordered relationships.

Which means it's not likely to be "fixed" anytime soon, as it would require changes to CloudKit and apple's iCloud generally (vs. just changes to CoreData).

So...

You have a few not-good choices:

  1. Don't have formal Relationships -- instead have a field where you store, e.g., a list of references. Maintain this yourself as you add/remove child objects. Essentially have an "admin" field in parent record that you use to maintain your own Relationship mechanism. (e.g.: generate a UUID for each child, parent stores a concatenated list of UUID's for all children)
  2. Use CoreData Relationships, and store ordering data in the child objects, e.g. orderIndex int field that you maintain manually.
  3. Mixed: use unordered Relationship from parent to children, and also store a field of "order of children" in parent. CoreData manages the relationship, and you can manually maintain and apply ordering of those children as needed.

No matter what you do, it's bad:

  1. No relationships: no "fetch parent" and get references to children via relationship; you have to fetch both separately. Also no "delete" rules done for you, e.g. " on delete parent, cascade delete children." This works when you need children in more than one collection (as each collection record keeps it's own list of children)

  2. Ordering in child objects (this is what I use): you have to insert yourself in every add and delete operation on children, running code to adjust orderIndex values of all children. I manage this by having a CoreDataManager.createChild(atIndex:), CoreDataManager.deleteChild() and CoreDataManager.moveChild(toIndex:) functions that apply side effect of updating orderIndex values. But at least you get "fetch parent; for c in parent.children do..." and cascade delete's back. However: now child can only be in one parent's list, as child only has one orderIndex value.

  3. Relationship + manually maintained ordering field in parent: let core data manage the association, and when you need ordered children you can use a parent.orderedChildren() function that reads your .childOrder field of parent, applies it to .children list. But... You still have to manage the parent's .childOrder field manually, and change it every time you add/remove/reorder children. HOWEVER, with Cloud Sync, there are many potential bugs wit your .child list and .childOrder field value getting out of sync as children are added/removed in different app instances. Keeping the child list and the ordering separate means they can be updated by cloud sync separately.

ManagedObjectContext does not 'get dirty' on edit of a nested array of objects

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

Sample Image

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.

Can I set usedWithCloudKit to false in core data contents

Check to see under "Configurations" if "Default" or any custom configurations you have in your Data Model file has "Used with CloudKit" checked. Should appear in the right panel when you select a configuration.

Sample Image

Core data + CloudKit - sharing between iOS and watchOS companion app

Resolved by adding a cloudKitContainerOptions to my context description like this :

class CoreDataStack {
static let persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "MyProjectName")
let description = container.persistentStoreDescriptions.first

description?.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "myCloudContainerID") // HERE !

container.loadPersistentStores(completionHandler: { (_, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()

}


Related Topics



Leave a reply



Submit