Core Data is probably loved as much as it is shunned by iOS developers. It is a framework of great power that often comes with great frustration. But it remains a popular tool among developers despite its pitfalls — likely because Apple continues to invest in it and encourages its adoption, as well as the availability of the many open-source libraries that make Core Data easier to use. Consider unit testing, and Core Data gets a bit more cumbersome. Luckily, there are established techniques to facilitate testing your models. Add Swift to this equation, and the learning curve gets slightly steeper.
Prefix managed object subclasses
Because Swift classes are namespaced, you must prefix the class name with the name of the module in which it is compiled. You can do this by opening your
.xcdatamodeld file in Xcode, selecting an entity, and opening the model entity inspector.
This is certainly easy to forget, and if you do, you’ll see this error:
CoreData: warning: Unable to load class named 'Person' for entity 'Person'. Class not found, using default NSManagedObject instead.
Not very helpful, is it? When I first saw this, it took me a few puzzling minutes to realize that I had forgotten to prefix my class names. And there’s another catch. You must add the module name prefix after you generate the classes, otherwise Xcode will not create the classes properly (or at all). This is a bug.
Implementing common managed object extensions
Most of the existing Objective-C Core Data libraries that you’ll find implement the following helper methods in some way, if not verbatim. These methods mitigate the awkwardness of inserting new objects into Core Data and avoid stringly-typed Objective-C.
I have decided to use Swift for one of my side projects, and in designing the model layer of the app my first thought was to rewrite these methods in Swift. Let’s see what that would look like.
entityName() function just became much less elegant. Remember, we have to prefix our Swift classes for Core Data which means their fully qualified names take the form
<ModuleName>.<ClassName>. This means we must parse out the entity name which is the class name only. This seems fragile and probably isn’t a good idea. Additionally, we have to use the
object_getClass() function from the Objective-C runtime library, which feels dirty — even in Objective-C. I’ve always avoided using such runtime voodoo as much as possible, opting for actual design patterns instead. Even
NSStringFromClass() feels wrong in Swift. And generally speaking, what do we gain by simply rewriting our old Objective-C code? Not much.
Despite these issues, I decided to let it be for the moment so that I could continue working and give some thought to a swifter design. I continued building out my model classes, standing up my core data stack, and writing unit tests. Much to my surprise, using the extension functions above crashed when running my unit tests. I ran the same code from the Application Target and everything was fine. After more investigation, I realized that I had just found a Swift compiler bug. You can find an example project on GitHub that exhibits the bug. The issue is that the following function incorrectly returns
nil in a project’s Test Target.
So much for revisiting these functions later. I could continue using them in the Application Target, but I would still need to find a fix for initializing managed objects in the Test Target. This isn’t ideal. I would rather have a single solution that works in both targets. Back to the drawing board.
Rethinking and redesigning
Let’s reiterate what we are trying to achieve. We want:
- To find a convenient way to initialize managed objects by encapsulating the use of
- To workaround the bug in
- To avoid having to pass literal entity names, like
"Person", to the initializer
- To avoid the issues mentioned above (using
- To conform to Swift paradigms and utilize Swift features
- To workaround the bug in
The solution that meets all the criteria above is a convenience initializer:
This is very similar to the original class factory function in the extension. It receives a context and returns a managed object. Regarding (2), it is very clear how this addresses the problematic
NSEntityDescription class function. In Swift, an initializer is guaranteed to return a non-nil, typed instance, whereas
insertNewObjectForEntityForName(_, inManagedObjectContext:) returns
AnyObject. We avoid having to cast the return value altogether.
As you’ve probably noticed, the entity name (
"Person") is hard-coded. And you are correct in concluding that this solution doesn’t generalize. That is, all of your managed object subclasses would need to implement this convenience initializer and provide their own value for the entity name. You might consider tweaking this by moving the convenience initializer to a new extension and replacing the hard-coded string with an
entityName() function that classes must override. Unfortunately, this will not work due to Swift’s initializer delegation and two-phase initialization enforcements.
In the end, I think adding these 3 lines of code to each of your managed object subclasses is a worthwhile exchange for type-safety and a more pure, more swift design. Perhaps this could eventually be automated via mogenerator or a similar tool. Cocoa may be dying, but it certainly isn’t dead yet. As we face these kinds of challenges with Swift, it is important to remember that the Objective-C way is not always the Swift way.
There’s one more thing. In trying to find ways around the
NSEntityDescription bug, I found an odd way to get the aforementioned extension functions to work in the Test Target. We know that unit testing in Swift is tricky because of its access control implementation. The files in your Application Target aren’t available to your Test Target because these are two different modules. The usual strategy is to add your files to both targets. If you do not do this, but instead make your managed object subclasses
public and import them to your Test Target (
import <AppTargetName>), then casting from
NSEntityDescription.insertNewObjectForEntityForName(_, inManagedObjectContext:) succeeds.