Deep copying an NSManagedObject means creating a manageable duplicate of an NSManagedObject (managed object) that includes all the data involved in every relational hierarchy of that managed object.
Although Core Data is a powerful object graph management framework, it doesn’t provide a direct method for copying managed objects. If developers want to create a duplicate of a managed object, the only feasible way is to write specific code that reads the properties of the original object and assigns them to a new object. This method is effective when the structure of the managed object is simple. However, once the structure becomes complex and relationships become numerous, the amount of code increases significantly and is prone to errors.
Developers have been looking for a convenient and universal tool to solve the deep copy issue for years, but until now, there hasn’t been a widely accepted solution.
I also encountered this problem while developing an app, needing to deep copy a managed object with a complex structure and extensive relational chains. Considering that I might face similar situations in the future, I decided to write a piece of code that is simple to use and widely applicable for my own use.
In this article, we will explore the technical challenges of deep copying NSManagedObject in Core Data, the thought process of solving these issues, and introduce a tool I wrote - MOCloner.
Challenges in Deep Copying NSManagedObject
Complex Relationship Structures
The following image is a part of the data model diagram for Health Notes. Although only a portion of the model relationships is selected, it nearly covers all types of relationships, including one-to-one, one-to-many, and many-to-many.
When copying a Note object, it involves hundreds or thousands of other objects in the relational chain. Achieving a fast and accurate deep copy of all data is quite challenging.
Selective Copying
Sometimes when deep copying, we might not need to copy all the data in every relationship level. We may want to ignore a particular branch at the n+1 level in the nth level.
Or, when copying a certain property (optional or with a default value) of a managed object, we might choose not to copy its content.
All these tasks should ideally be handled during the deep copy process.
Data Validity
Some properties in managed objects have uniqueness or timeliness, which need special treatment in deep copying.
For example:
- In the above diagram, the type of Note’s id is UUID. During deep copying, the original content should not be copied, and a new data should be created for the new object.
- The NoteID in Item should correspond to the id of Note. How to maintain this consistency during copying?
- The createDate in ItemDate should be the time of record creation. How to set it to the date of deep copying?
If similar issues can’t be handled during deep copying, adjusting them after copying will be challenging when dealing with large data volumes.
Inverse Many-to-Many Relationships
In the diagram above, Tag and Memo have a many-to-many relationship. When an inverse many-to-many situation (like Tag) appears in a relationship chain, it needs to be handled with extra caution. From a business logic perspective, Tag does not belong to a specific branch of a Note, which has always been a difficult problem in Core Data data synchronization.
Solution Approach for Deep Copying
Despite the various challenges, it is still possible to address them using the numerous tools provided by Core Data.
Making Good Use of Description
In Xcode, when using the Data Model Editor to create a data model, it is compiled into a .momd
file and saved in the Bundle. When creating NSPersistentContainer
, NSManagedObjectModel
uses this file to translate the model definition into implementation. Developers can access various Descriptions provided by Core Data to get the necessary information.
The most commonly used Description might be NSPersistentStoreDescription
, from which you can get Config or set iCloud options (for more information, refer to Mastering Core Data Stack).
Other Descriptions include, but are not limited to:
NSEntityDescription
- Description of the entity.NSRelationshipDescription
- Description of the entity relationships.NSAttributeDescription
- Description of the entity attributes.NSFetchIndexDescription
- Description of the indexes.NSDerivedAttributeDescription
- Description of derived attributes.
The following code creates a new object with the same structure as a given managed object using NSEntityDescription
:
guard let context = originalObject.managedObjectContext else {
throw CloneNSManagedObjectError.contextError
}
// create clone NSManagedObject
guard let entityName = originalObject.entity.name else {
throw CloneNSManagedObjectError.entityNameError
}
let cloneObject = NSEntityDescription.insertNewObject(
forEntityName: entityName,
into: context
)
To get all property descriptions of the managed object through NSAttributeDescription
:
let attributes = originalObject.entity.attributesByName
for (attributeName, attributeDescription) in attributes {
...
}
To traverse all relationship descriptions of the managed object using NSRelationshipDescription
:
let relationships = originalObject.entity.relationshipsByName
for (relationshipName, relationshipDescription) in relationships {
...
}
To get the entity corresponding to the inverse relationship description:
let inverseEntity = relationshipDescription.inverseRelationship?.entity
These Descriptions are the cornerstone for developing generic code for deep copying NSManagedObject.
Using userinfo to Pass Information
To address issues like selective copying and data validity mentioned above, it’s necessary to provide sufficient information during deep copying.
Since this information may be distributed across various levels of the relationship chain, the most direct and effective way is to add corresponding content in the User Info provided by Xcode’s data model editor.
Every developer who has used Xcode’s data model editor should have seen the User Info input box on the right side. Through this box, we can set information for Entity, Attribute, and Relationship and extract it from the corresponding Description.
The following code checks if there is an exclusion flag in the userinfo of the Attribute:
if let userInfo = attributeDescription.userInfo {
// Check if the "exclude" flag is added to this attribute
// Only determine whether the Key is "exclude" or not, do not care about the Value
if userInfo[config.exclude] != nil {
if attributeDescription.isOptional || attributeDescription.defaultValue != nil {
continue
} else {
throw CloneNSManagedObjectError.attributeExcludeError
}
}
}
The following code creates a new UUID for Attributes with rebuild : uuid
flag in userinfo (of type UUID):
if let action = userInfo[config.rebuild] as? String {
switch action {
case "uuid":
if attributeDescription.attributeType == NSAttributeType.UUIDAttributeType {
newValue = UUID()
} else {
throw CloneNSManagedObjectError.uuidTypeError
}
...
default:
break
}
}
setPrimitiveValue and setValue
In Core Data development, setPrimitiveValue
is often used in various situations. For example, it’s used in awakeFromInsert
to set initial values for properties, and in willSave
to check the validity of property values. Especially when we cannot directly call properties of the managed object instance, using setPrimitiveValue
can conveniently set the value using the attribute name.
for (attributeName, attributeDescription) in attributes {
var newValue = originalObject.primitiveValue(forKey: attributeName)
cloneObject.setPrimitiveValue(newValue, forKey: attributeName)
}
Since setPrimitiveValue
accesses the managed object’s raw value (bypassing the snapshot), it is more efficient and does not trigger KVO observation.
However, setPrimitiveValue
also has its drawbacks—it does not automatically handle inverse relationships. When using it to set relationship content, work must be done on both sides of the relationship, which can significantly increase the amount of code.
For managed object instances, in most cases, Core Data generated relationship management methods are usually directly used for relationship operations, for example:
@objc(addItemsObject:)
@NSManaged public func addToItems(_ value: Item)
@objc(removeItemsObject:)
@NSManaged public func removeFromItems(_ value: Item)
@objc(addItems:)
@NSManaged public func addToItems(_ values: NSSet)
@objc(removeItems:)
@NSManaged public func removeFromItems(_ values: NSSet)
// Note and Item have a one-to-many relationship
let note = Note(context: viewContext)
let item = Item(context: viewContext)
note.addToItems(item)
item.note = note
In generic deep copy code, we cannot directly use these system-provided methods, but we can use setValue
to set relationship data.
setValue
will internally find the corresponding Setter to manage the bidirectional relationship.
Below is the code for setting to-one relationships:
if !relationshipDescription.isToMany,
let originalToOneObject = originalObject.primitiveValue(forKey: relationshipName) as? NSManagedObject {
let newToOneObject = try cloneNSMangedObject(
originalToOneObject,
parentObject: originalObject,
parentCloneObject: cloneObject,
excludedRelationshipNames: passingExclusionList ? excludedRelationshipNames : [],
saveBeforeReturn: false,
root: false,
config: config
)
cloneObject.setValue(newToOneObject, forKey: relationshipName)
}
NSSet and NSOrderedSet
In Core Data, to-many relationships are represented in the generated NSManagedObject Subclass code as NSSet?
. However, if the to-many relationship is set to ordered, its type changes to NSOrderedSet?
.
By checking NSRelationshipDescription
’s isOrdered
, you can select the correct corresponding type. For example:
if relationshipDescription.isOrdered {
if let originalToManyObjects = (originalObject.primitiveValue(forKey: relationshipName) as? NSOrderedSet) {
for needToCloneObject in originalToManyObjects {
if let object = needToCloneObject as? NSManagedObject {
let newObject = try cloneNSMangedObject(
object,
parentObject: originalObject,
parentCloneObject: cloneObject,
excludedRelationshipNames: passingExclusionList ? excludedRelationshipNames : [],
saveBeforeReturn: false,
root: false,
config: config
)
newToManyObjects.append(newObject)
}
}
}
}
Handling Inverse Many-to-Many Relationships
When moving down the relationship chain, if an inverse relationship of an entity is many-to-many, it creates a tricky situation during deep copying—entities with inverse many-to-many relationships serve the entire forward relationship tree.
For example, in the previous diagram of Memo and Tag, a memo can correspond to multiple tags, and a tag can correspond to multiple memos. If we deep copy from Note down to Memo and continue to copy Tag, it would contradict the original design intent of Tag.
The solution is, when encountering an entity A in the relationship chain with an inverse many-to-many relationship, to no longer continue deep copying downwards. Instead, add the newly copied managed object to the relationship with entity A, fulfilling the data model’s design intention.
if let inverseRelDesc = relationshipDescription.inverseRelationship, inverseRelDesc.isToMany {
let relationshipObjects = originalObject.primitiveValue(forKey: relationshipName)
cloneObject.setValue(relationshipObjects, forKey: relationshipName)
}
Deep Copying with MOCloner
Integrating the above approach, I wrote a library for deep copying NSManagedObject in Core Data — MOCloner.
MOCloner Overview
MOCloner is a small library designed to achieve customizable deep copying of NSManagedObject. It supports one-to-one, one-to-many, and many-to-many relationships. In addition to faithful copying of original data, it also provides selective copying and the generation of new values during the copy.
Basic Demonstration
Creating a deep copy of the Note in the above diagram:
let cloneNote = try! MOCloner().clone(object: note) as! Note
Deep copying from the middle of the relationship chain downwards (not copying the upper part of the chain):
// Add the names of relationships to ignore in excludedRelationshipNames
let cloneItem = try! MOCloner().clone(object: item, excludedRelationshipNames: ["note"]) as! Item
Customization
MOCloner uses key-value pairs added in Xcode’s Data Model Editor’s User Info for customizing the deep copy process. Currently, it supports the following commands:
-
exclude
This key can be set in Attribute or Relationship. The presence of the
exclude
key, regardless of its value, will enable the exclusion logic.When set in Attribute’s userinfo, deep copying will not copy the original object’s attribute value (requires the Attribute to be Optional or have a Default value).
When set in Relationship’s userinfo, deep copying will ignore all relationships and data under this relationship branch.
For convenience in situations not suitable for setting in userinfo (e.g., deep copying from the middle of a relationship chain), you can also add the names of relationships to exclude in the
excludedRelationshipNames
parameter (as in Basic Demonstration 2).
-
rebuild
Used to dynamically generate new data during deep copying. Only for setting Attributes. Currently supports two values:
uuid
andnow
.uuid
: For Attributes of type UUID, creates a new UUID during deep copying.now
: For Attributes of type Date, sets the attribute to the current date (Date.now) during deep copying.
-
followParent
A simplified version of Derived. Only for setting Attributes. Allows specifying an Attribute in an entity lower in the relationship chain to get the value of a corresponding Attribute from a managed object instance higher in the chain (requires both Attributes to be of the same type). In the diagram below, Item’s
noteID
will get the value of Note’sid
.
-
withoutParent
Used only in conjunction with
followParent
. Addresses situations when deep copying from the middle of a relationship chain, wherefollowParent
is set but the ParentObject cannot be obtained.When
withoutParent
iskeep
, it maintains the original value of the object being copied.When
withoutParent
isblank
, it leaves the value unset (requires the Attribute to be Optional or have a Default value).
If the key names in userinfo conflict with those already used in your project, you can customize them using MOClonerUserInfoKeyConfig
.
let moConfig = MOCloner.MOClonerUserInfoKeyConfig(
rebuild: "newRebuild", // new Key Name
followParent: "followParent",
withoutParent: "withoutParent",
exclude: "exclude"
)
let cloneNote = try cloner.clone(object: note, config: moConfig) as! Note
System Requirements
MOCloner requires macOS 10.13, iOS 11, tvOS 11, watchOS 4, or higher.
Installation
MOCloner is distributed using Swift Package Manager. To use it in another Swift package, add it as a dependency in your Package.swift.
let package = Package(
...
dependencies: [
.package(url: "https://github.com/fatbobman/MOCloner.git", from: "0.1.0")
],
...
)
To use MOCloner in an application, add it to your project using Xcode’s File > Add Packages…
import MOCloner
Given that MOCloner is just a few hundred lines of code, you can also copy the code directly into your project for use.
Considerations When Using MOCloner
Perform in a Private Context
When deep copying involves a large amount of data, it’s advisable to perform the operation in a private context to avoid occupying the main thread.
It’s recommended to use NSManagedObjectID
for data transfer before and after the deep copy operation.
Memory Usage
Deep copying managed objects that involve a large amount of relational data may result in significant memory usage. This is particularly evident when dealing with binary type data (such as saving large amounts of image data in SQLite). Consider the following methods to control memory usage:
- Temporarily exclude attributes or relationships that consume a lot of memory during deep copying. After deep copying, add them one by one using other code.
- When deep copying multiple managed objects, consider performing them one by one using
performBackgroundTask
.
Version and Support
MOCloner is licensed under the MIT license, allowing you free usage in your projects. However, please note that MOCloner does not come with any official support channels.
Core Data offers a wide range of functionalities and options, enabling developers to create a vast array of different relationship graphs. MOCloner has been tested only in a subset of these scenarios. Therefore, before you start preparing to use MOCloner in your project, it is strongly recommended that you spend some time getting familiar with its implementation and conduct more unit tests to avoid any potential data error issues.
If you encounter any problems, errors, or wish to suggest improvements, please create Issues or Pull Requests on GitHub.
Conclusion
Deep copying NSManagedObject is not a common functionality requirement. However, having an easy-to-use solution like MOCloner might inspire you to try new design ideas in your Core Data projects.