Understanding the differences between your Core Data model and managed objects
Published on: October 5, 2020You may have noticed that when Xcode generates your NSManagedObject
classes based on your Core Data model file, most of your managed object's properties are optional. Even if you've made them required in the model editor, Xcode will generate a managed object where most properties are optional.
In this article we'll explore this phenomenon, and why it happens.
Exploring generated NSManagedObject
subclasses
When you build a project that uses Xcode's automatic code generation for Core Data models, your NSManagedObject
subclasses are generated when you build your project. These classes are written your project's Derived Data folder and you shouldn't modify them directly. The models that are generated by Xcode will have optional properties for some of the properties that you've added to your entity, regardless of whether you made the property optional in the model editor.
While this might sounds strange at first, it's actually not that strange. Take a look at the following NSManagedObject
subclass:
extension ToDoItem {
@nonobjc public class func fetchRequest() -> NSFetchRequest<ToDoItem> {
return NSFetchRequest<ToDoItem>(entityName: "ToDoItem")
}
@NSManaged public var completed: Bool
@NSManaged public var label: String?
}
One of the two properties for my ToDoItem
is optional even they're both required in the model editor. When I create an instance of this ToDoItem
, I'd use the following code:
let item = ToDoItem(context: managedObjectContext)
A managed object's initializer takes a managed object context. This means that I don't assign a value to the managed properties during the initialization of the ToDoItem
. Printing the value for both the label
and completed
properties yields and interesting result:
print(item.label) // nil
print(item.completed) // false
While label
is nil
as expected, Core Data assigned a default value of false
to the completed
property which makes sense because Xcode generated a non-optional property for completed
. Let's take it a step further and take a look at the following code:
let item = ToDoItem(context: managedObjectContext)
item.label = "Hello, item"
print(item.label) // "Hello, item"
print(item.completed) // false
do {
try managedObjectContext.save()
} catch {
print(error)
}
When you run this code, you'll find that it produces the following output:
Optional("Hello, item")
false
Error Domain=NSCocoaErrorDomain Code=1570 "completed is a required value." UserInfo={NSValidationErrorObject=<CoreDataExample.ToDoItem: 0x60000131c910> (entity: ToDoItem; id: 0x600003079900 <x-coredata:///ToDoItem/t1FABF4F1-0EF4-4CE8-863C-A815AA5C42FF2>; data: {
completed = nil;
label = "Hello, item";
}), NSValidationErrorKey=completed, NSLocalizedDescription=completed is a required value.}
This error clearly says completed is a required value.
which implies that completed
isn't set, and the printed managed object that's shown alongside the error message also shows that completed
is nil
. So while there is some kind of a default value present for completed
, it is not considered non-nil
until it's explicitly assigned.
To understand what's happening, we can assign a value to completed
and take a look at the printed description for item
again:
let item = ToDoItem(context: managedObjectContext)
item.label = "Hello, item"
print(item.completed)
print(item)
This code produces the following output:
false
<CoreDataExample.ToDoItem: 0x6000038749b0> (entity: ToDoItem; id: 0x600001b576e0 <x-coredata:///ToDoItem/tD27C9C9D-A676-4280-9D7C-A1E154B2AD752>; data: {
completed = 0;
label = "Hello, item";
})
This is quite interesting, isn't it?
The completed
property is defined as a Bool
, yet it's printed as a number. We can find the reason for this in the underlying SQLite store. The easiest way to explore your Core Data store's SQLite file is by passing -com.apple.CoreData.SQLDebug 1
as a launch argument to your app and opening the SQLite file that Core Data connects to in an SQLite explorer like SQLite database browser.
Tip:
Learn more about Core Data launch arguments in this post.
When you look at the schema definition for ZTODOITEM
you'll find that it uses INTEGER
as the type for ZCOMPLETED
. This means that the completed
property is stored as an integer in the underlying SQLite store. The reason completed
is stored as an INTEGER
is simple. SQLite does not have a BOOLEAN
type and uses an INTEGER
value of 0 to represent false
, and 1 to represent true
instead.
The data
that you see printed when you print your managed object instance isn't the value for your completed
property, it's the value for completed
that will be written to the SQLite store.
There are two things to be learned from this section.
First, you now know that there is a mismatch between the optionality of your defined Core Data model and the generated managed objects. A non-optional String
is represented as an optional String
in your generated model while a non-optional Bool
is represented as a non-optional Bool
in your generated model.
Second, you learned that there's a difference between how a value is represented in your managed object model versus how it's represented in the underlying SQLite store. To see which values are used to write your managed object instance to the underlying storage you can print the managed object and read the data
field in the printed output.
The main lesson here is that your Core Data model in the model editor and your managed object subclasses do not represent data the same way. Optional in your Core Data model does not always mean optional in your managed object subclass and vice versa. A non-optional value in your Core Data model may be represented as an optional value in your managed object subclass. Core Data will validate your managed object against its managed object model when you attempt to write it to the persistent store and throw errors if it encounters any validation errors.
So why does this mismatch exist? Wouldn't it be much easier if the managed object model and managed object subclasses had a direct mapping?
Understanding the mismatch between managed objects and the Core Data model
A big part of the reason why there's a mismatch between your managed objects and the model you've defined in the model editor comes from Core Data's Objective-C roots.
Since Objective-C doesn't deal with Optional
at all there isn't always a good mapping from the model definition to Swift code. Oftentimes, the way the mapping works seems somewhat arbitraty. For example, Optional<String>
and Optional<Bool>
both can't be represented as a type in Objective-C for the simple reason that Optional
doesn't exist in Objective-C. However, Swift and Objective-C can interop with each other and Optional<String>
can be bridged to an NSString
automatically. Unfortunately Optional<Bool>
can't be mapped to anything in Objective-C automatically as Xcode will tell you when you attempt to define an @NSManaged
property as Bool?
.
If you've never worked with Objective-C it might seem very strange to you that there is no concept of Optional
. How did folks use optional properties in Core Data before Swift? And what happens when something is supposed to be nil
in Objective-C?
In Objective-C it's perfectly fine for any value to be nil
, even when you don't expect it. And since Core Data has its roots in Objective-C some of this legacy carries over to your generated Swift classes in a sometimes less than ideal manner.
The most important takeaway here isn't how Objective-C works, or how Xcode generates code exactly. Instead, I want you to remember that the types and configuration in your Core Data model definition do not (have to) match the types in your (generated) managed object subclass.
In Summary
In this week's article you've learned a lot about how your managed object subclasses and Core Data model definition don't always line up the way you'd expect them to. You saw that sometimes a non-optional property in the model editor can end up as optional in the generated managed object subclass, and other times it ends up as a non-optional property with a default value even if you didn't assign a default value yourself.
You also saw that if a default value is present on a managed object instance it doesn't mean that the value is actually present at the time you save your managed object unless you explicitly defined a default value in the Core Data model editor.
While this is certainly confusing and unfortunate, Core Data is pretty good at telling you what's wrong in the errors it throws while saving a managed object. It's also possible to inspect the values that Core Data will attempt to store by printing your managed object instance and inspecting its data
attribute.
On a personal note I hope that the behavior I described in this week's article is addressed in a future update to Core Data that makes it more Swift friendly where the managed object subclasses have a closer, possibly direct mapping to the Core Data model that's defined in a model editor. But until then, it's important to understand that the model editor and your managed object subclasses do not represent your model in the same way, and that this is at least partially related to Core Data's Objective-C roots.
If you have any questions, corrections or feedback about this post please let me know on Twitter. This post is part of some of the research, exploration and preparation that I'm doing for a book about Core Data that I'm working on. For updates about this book make sure to follow me on Twitter. I'm currently planning to release the book around the end of 2020.