Skip to content

Instantly share code, notes, and snippets.

@beader
Created December 29, 2022 08:17
Show Gist options
  • Save beader/32ed1386b994970ea458c4fcd64fc4a1 to your computer and use it in GitHub Desktop.
Save beader/32ed1386b994970ea458c4fcd64fc4a1 to your computer and use it in GitHub Desktop.

ObjectGraphCloner

If you are using CoreData with Cloudkit, after a NSManagedObject being shared, a new zone is created and being shared.

All objects associated with the object (in the same connected component in the object graph) are also putted into this zone.

When the owner stops sharing this object, the local cache about the share will not be updated and the object will not be able to be shared again. (Unless the user reinstall the app)

The issue has been reported in https://developer.apple.com/forums/thread/689774

As Fat Xu metioned in his blog, one walkaround method is to clone the whole connected compoenent the objected is belonged to, then delete the origin object.

Usage

do {
    // object: NSManagedObject
    let cloner = ObjectGraphCloner()
    let clonedObject = try cloner.cloneObjectGraph(object)
    // viewContext.delete(object)
    try viewContext.save()
} catch {
    viewContext.rollback()
    print("Clone failed: \(error.localizedDescription)")
}       
//
// ObjectGraphCloner.swift
//
//
// Created by beader on 2022/12/29.
//
import Foundation
import CoreData
class ObjectGraphCloner {
typealias OriginObject = NSManagedObject
typealias ClonedObject = NSManagedObject
enum CloneError: Error {
case contextError
case entityNameError
case castError
case missingObjectError
}
// Mapping from origin objects to new cloned objects
var clonedObjects: [OriginObject: ClonedObject] = [:]
func cloneObjectGraph(_ object: OriginObject) throws -> ClonedObject {
try traverseAndCloneObjects(from: object)
try traverseAndCloneRelationships()
return clonedObjects[object]!
}
/// Traverse from source node and clone objects in graph without relationships
private func traverseAndCloneObjects(from object: OriginObject) throws {
// if object is already cloned, stop traversing from this node
guard !clonedObjects.keys.contains(object) else {
return
}
clonedObjects[object] = try cloneObjectAttributes(object)
let relationships = object.entity.relationshipsByName
for (relationshipName, relationshipDescription) in relationships {
for destination in extractDestinationSet(from: object, relationshipName: relationshipName, relationshipDescription: relationshipDescription) {
if let destination = destination as? NSManagedObject {
try traverseAndCloneObjects(from: destination)
}
}
}
}
/// Clone relationships from origin objects to cloned objects
private func traverseAndCloneRelationships() throws {
for (originObject, clonedObject) in clonedObjects {
try cloneRelationships(originObject: originObject, clonedObject: clonedObject)
}
}
private func cloneObjectAttributes(_ object: OriginObject) throws -> ClonedObject {
guard let context = object.managedObjectContext else {
throw CloneError.contextError
}
guard let entityName = object.entity.name else {
throw CloneError.entityNameError
}
let clonedObject = NSEntityDescription.insertNewObject(
forEntityName: entityName,
into: context
)
let attributes = object.entity.attributesByName
for (attributeName, _) in attributes {
let attributeValue = object.primitiveValue(forKey: attributeName)
clonedObject.setValue(attributeValue, forKey: attributeName)
}
return clonedObject
}
private func cloneRelationships(originObject: OriginObject, clonedObject: ClonedObject) throws {
let relationships = originObject.entity.relationshipsByName
for (relationshipName, relationshipDescription) in relationships {
let isToMany = relationshipDescription.isToMany
let isOrdered = relationshipDescription.isOrdered
if isToMany {
if isOrdered {
let destinations = destinationObjectOrderedSet(for: originObject, relationshipName: relationshipName)
let clonedDestinations = try getClonedObjects(for: destinations)
clonedObject.setValue(clonedDestinations, forKey: relationshipName)
} else {
let destinations = destinationObjectSet(for: originObject, relationshipName: relationshipName)
let clonedDestinations = try getClonedObjects(for: destinations)
clonedObject.setValue(clonedDestinations, forKey: relationshipName)
}
} else {
if let destination = destinationObject(for: originObject, relationshipName: relationshipName) {
if let clonedDestination = clonedObjects[destination] {
clonedObject.setValue(clonedDestination, forKey: relationshipName)
} else {
throw CloneError.missingObjectError
}
}
}
}
}
private func destinationObject(for object: OriginObject, relationshipName: String) -> NSManagedObject? {
object.primitiveValue(forKey: relationshipName) as? NSManagedObject
}
private func destinationObjectSet(for object: OriginObject, relationshipName: String) -> NSSet? {
object.primitiveValue(forKey: relationshipName) as? NSSet
}
private func destinationObjectOrderedSet(for object: OriginObject, relationshipName: String) -> NSOrderedSet? {
object.primitiveValue(forKey: relationshipName) as? NSOrderedSet
}
private func getClonedObjects(for objects: NSSet?) throws -> NSSet? {
guard let objects else { return nil }
var clonedObjects = [NSManagedObject]()
for object in objects {
if let object = object as? NSManagedObject {
if let clonedObject = self.clonedObjects[object] {
clonedObjects.append(clonedObject)
} else {
throw CloneError.missingObjectError
}
} else {
throw CloneError.castError
}
}
return NSSet(array: clonedObjects)
}
private func getClonedObjects(for objects: NSOrderedSet?) throws -> NSOrderedSet? {
guard let objects else { return nil }
var clonedObjects = [NSManagedObject]()
for object in objects {
if let object = object as? NSManagedObject {
clonedObjects.append(object)
} else {
throw CloneError.castError
}
}
return NSOrderedSet(array: clonedObjects)
}
private func extractDestinationSet(from object: OriginObject, relationshipName: String, relationshipDescription: NSRelationshipDescription) -> NSSet {
let destinations: NSSet
if relationshipDescription.isToMany {
destinations = object.primitiveValue(forKey: relationshipName) as? NSSet ?? []
} else if let destination = object.primitiveValue(forKey: relationshipName) {
destinations = NSSet(object: destination)
} else {
destinations = []
}
return destinations
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment