Handling optional scalar Core Data attributes
Voodoo called @NSManaged is not always your friend
Core Data class files in Swift have one distinct issue when you use optional entity attributes of any scalar type: Int64, Double, Bool etc.
Ideally, they should be modeled in the class file as:
@NSManaged public var counter: Int64?
However, this is not allowed because Objective-C scalar types do not have a notion of nil
value. Thus you get this error:
Property cannot be marked @NSManaged because its type cannot be represented in Objective-C
Hence you can’t mark the attribute to be both Optional and “Use Scalar Type” in the modeler. My first approach in this situation is to look for suitable default value for the attribute, set that and mark it as non-optional. If I can do that - problem solved.
In cases where that’s not possible, we need something else.
Replacing @NSManaged
@NSManaged
marker above is some really powerful voodoo. First – it allows you to define stored property in the class extension – something not otherwise allowed by Swift compiler.
Second – it means that getter and setter for this property will be dynamically generated in the runtime. (Which is why Swift compiler is letting you do it.)
Because it’s essentially a promise given to the compiler, we can remove the marker and implement the getter/setter on our own. Since we are now totally in control, we can implement the attribute as proper Swift optional.
public var counter: Int64? {
get {
willAccessValue(forKey: "counter")
defer { didAccessValue(forKey: "counter") }
return primitiveValue(forKey: "counter") as? Int64
}
set {
willChangeValue(forKey: "counter")
defer { didChangeValue(forKey: "counter") }
guard let value = newValue else {
setPrimitiveValue(nil, forKey: "counter")
return
}
setPrimitiveValue(value, forKey: "counter")
}
}
The KVO calls are there to replicate what Core Data would do, thus problem solved.
Using mogenerator
Code like this is nightmare to maintain by hand, very prone to accidental typos. Which is why I never code these by hand and instead rely on custom-made mogenerator templates.
MOGenerator last public build (1.31) doesn’t support “Use Scalar Type” flag, even though that’s already implemented and merged. I have built from the master late last year and use that for all my projects in past several months. Works great.
I also use my own copy of the MOGenerator templates, since default ones are a bit obsolete. Most importantly: I have removed all uses of NSSet
for to-many relationships and use non-optional Set<EntityName>
in there. I have also heavily simplified it and removed stuff I never use, like Fetched Properties and Abstract Entity.
The mogenerator build and the templates are available in the RTSwiftCoreDataStack repo on GitHub. It’s very simple to use them, here’s Run Script Phase from the Example app:
cd DataModel
"${SRCROOT}/DataModel/tmpl/mogenerator" --swift --model Example.xcdatamodeld --template-path tmpl --human-dir mogen-classes --machine-dir mogen-properties
This is part of the larger update of that library, which I’ll explain in the upcoming post.