Have you ever been presented with this Swift compiler error?
error: property cannot be marked @NSManaged because its type cannot be represented in Objective-C
… and did you (after questioning whether it’s really too late to become a florist) then change the property’s type to something compatible with Objective-C?
Or maybe you rely on Xcode to generate your Swift NSManagedObject
subclasses, so all of your modelled properties are automatically a type that’s compatible with Objective-C?
If so, then read on, because “property cannot be marked @NSManaged
” is actually why I think Swift is the most satisfying language for using Core Data.
It’s not your fault.
I didn’t always feel this way. In fact, I used to think that Core Data was an example of how the power of the Objective-C runtime and the power of the Swift type system could combine to become a clunky mess.
Swift 2.0 opened up more possibilities for expressively using Core Data, such as Rob Rhyne’s protocol extensions for fetching managed objects, but a significant source of “clunkiness” remains: the limitations of @NSManaged
.
Apple’s documentation tells us this about @NSManaged
:
Core Data provides the underlying storage and implementation of properties in subclasses of the NSManagedObject class. […] You use the @NSManaged attribute to inform the Swift compiler that Core Data provides the storage and implementation of a declaration at runtime.
Add the @NSManaged attribute to each property or method declaration in your managed object subclass that corresponds to an attribute or relationship in your Core Data model.
In the strictest sense, that’s all true, but it does imply that you’re constrained to using @NSManaged
and the types it supports. Many developers accept those constraints and stop there, which is a shame.
A more complete description may shine a light on the possibilities that are available whenever a “property cannot be marked @NSManaged
”.
- Core Data provides the underlying storage and, unless an implementation is already present, a default implementation of properties in subclasses of the NSManagedObject class.
- You use the
@NSManaged
attribute to inform the Swift compiler that the Objective-C implementation of the declaration will be available at runtime (just as you would add declarations to an Objective-C Category’s@interface
). - Add the
@NSManaged
attribute to each property or method declaration in your managed object subclass that corresponds to an attribute or relationship if the default runtime-generated implementation suits your needs.
In other words, you only mark something as @NSManaged
when Core Data’s dynamically-generated Objective-C implementation is useful to you.
If it’s not useful to you, such as when a “property cannot be marked @NSManaged
”, you’re entirely free to provide your own accessors.
You can take charge of your accessors.
Consider a Token
entity with a single attribute called expiresAfter
, which has been configured in the Core Data model editor to be a Double
.
The @NSManaged
property for expiresAfter
can be declared either as an NSNumber?
or as a Double
depending on whether or not it can meaningfully be null
.
In this case, expiresAfter
needs to be nullable, so the default Core Data generated accessors would look something like this:
class Token: NSManagedObject {
@NSManaged var expiresAfter: NSNumber?
@NSManaged var primitiveExpiresAfter: NSNumber?
}
Are you surprised by the primitiveExpiresAfter
property? Similar ‘primitive’ accessors are generated for each of your attributes and relations.
These generated primitive accessors are responsible for getting and setting the private internal storage value for their property, and they are provided so that you can write your own custom property accessors.
As with everything else generated by Core Data which you intend to use from your own code, you declare primitive accessors as @NSManaged
to inform the compiler that the Objective-C implementation of the declaration will be available at runtime.
Here’s a custom implementation of expiresAfter
which is similar to what Core Data generates by default:
class Token: NSManagedObject {
var expiresAfter: NSNumber? {
get {
willAccessValueForKey("expiresAfter")
defer { didAccessValueForKey("expiresAfter") }
return primitiveExpiresAfter
}
set {
willChangeValueForKey("expiresAfter")
defer { didChangeValueForKey("expiresAfter") }
primitiveExpiresAfter = newValue
}
}
@NSManaged private var primitiveExpiresAfter: NSNumber?
}
Just like the Core Data runtime-generated accessor, expiresAfter
in this version is a computed property which relies on primitiveExpiresAfter
to interact with the internal storage values.
The bodies for get
and set
show the additional ceremony that is required by Core Data whenever you interact with primitive values:
- Each read access of a primitive value must be bracketed by an appropriate
willAccessValueForKey:
anddidAccessValueForKey:
. - Each write access of a primitive value must be bracketed by an appropriate
willChangeValueForKey:
anddidChangeValueForKey:
.
The primitiveExpiresAfter
declaration has also been changed: it’s now private
, which aligns nicely with the fact that it’s accessing private internal storage.
Finally, here’s a custom implementation of expiresAfter
that supports a Double?
type:
class Token: NSManagedObject {
var expiresAfter: Double? {
get {
willAccessValueForKey("expiresAfter")
defer { didAccessValueForKey("expiresAfter") }
return primitiveExpiresAfter?.doubleValue
}
set {
willChangeValueForKey("expiresAfter")
defer { didChangeValueForKey("expiresAfter") }
primitiveExpiresAfter = newValue.map({NSNumber(double: $0)})
}
}
@NSManaged private var primitiveExpiresAfter: NSNumber?
}
This version is the same as the previous one, with the exception that in this version, our expiresAfter
computed property transparently converts Double?
values to NSNumber?
and vice-versa.
The expiresAfter
property could have just as successfully been declared as a Swift enum
or… anything really, as long as it could be converted to and from the data type required by the primitive accessor.
Note: Primitive accessors are always nullable Objective-C reference types rather than, as their name would imply, Objective-C “primitive types”. Whenever you see the ‘primitive’ prefix in an NSManagedObject
subclass, think ‘private’.
At this point, some of you may be unsettled: What if you need to interact with expiresAfter
from Objective-C code? Won’t this break everything for Objective-C callers?
It won’t break everything for Objective-C callers.
The explanation for why it won’t break everything can be found in the answer to this question:
Now that expiresAfter
has been declared as a Double?
in Swift, will Core Data still dynamically generate an accessor implementation for it at runtime?
If you’re not sure, here’s a clue:
error: property cannot be marked @NSManaged because its type cannot be represented in Objective-C
If Double?
is a type that cannot be represented in Objective-C, then the swift compiler will not expose the implementation for var expiresAfter: Double?
to Objective-C.
If our implementation is not exposed to Objective-C, then Core Data will not find it at runtime, which means that, yes, Core Data will dynamically generate the accessor implementation for us.
You’re free to interact with that implementation using KVC methods (valueForKey:
and setValue:forKey:
) from both Swift and Objective-C.
You can also directly interact with the Core Data generated accessor from Objective-C by declaring the property in an Objective-C Category @interface
that is not bridged to Swift.
You got peanut butter on my chocolate!
This is why I now consider Swift to be the most satisfying language for using Core Data. The fact that (more expressive) types can’t be represented in Objective-C is a benefit, not a limitation.
Swift NSManagedObject
subclasses can provide Swift-only accessors which happily coexist with Core Data’s dynamically-generated ones, and that feels like the best of both worlds.
You may want to automate this sort of thing.
For the most part, custom accessors which just perform mechanical type coercion are entirely boilerplate, so they’re perfectly suited for automatic generation by a tool like mogenerator (source).
Though mogenerator’s templates can be customized, the current version is incapable of generating a Swift computed property like expiresAfter
. The templates are also rather cluttered with conditionals and backwards-compatible code not relevant to a Swift 2.0+ project.
My own preference is for logic-less templates, so I’ve moved away from tinkering with mogenerator, but I’m sure it’s only a matter of time before their Swift templates get an overhaul.
If you’re impatient, you may want to pitch in and contribute to the project.