Painless Core Data in Swift
Core Data is large and powerful framework. Learn how to use it properly and efficiently in iOS app of any complexity.
Core Data is one of my favorite tools but I rarely write about it. Recently Dave DeLong and Marcus Zarra wrote about the topic which nudged me in turn.
I’ll explain how I use Core Data framework in my apps. You’ll see it fits really nicely with my general approach to coding: “write as little code as possible”. General guidelines:
- Make the model as small as it can be
- Add only those entities you intend to persist across app cycles
- Don’t write class files manually – generate them using some tool
- Never perform multiple writes in parallel, use serial non-concurrent operation queue
- Use your main MOC for read-only operations in 95% of cases, often even 100%
- Move writes and data import into background MOCs using separate PSC
- Use Swift to the max to simplify common operations
Model
Core Data is not relational database, it’s an object graph. Which means that you don’t have foreign keys like in RDB, you have actual object-to-object relationships.
Which requires you to make sure both sides of the relationships are properly declared and populated after the save is done. Do not ever create a one-way relationship, you’re asking for trouble. Using Core data modeler in Xcode, make sure to always setup deletion rules where you will most likely always have either Nullify
or Cascade
. If you have done that, you need to setup only one side of the relationship in code, Core Data will populate the other end automatically during context save.
Do not add entities you don’t need to persist across app starts. This is obvious at first glance but it’s very easy to fall into a trap of adding model entities for ad-hoc objects which have relationships to long-term Core Data-based objects. You don’t have to do that and you can freely connect them in memory where I actually recommend to use one-way relationships.
Say you have a Cart
object with a set of CartItem
objects each 1:1 related to Product
. Product
needs to be persisted so it will be NSManagedObject
based. But if the Cart
is valid only during one app cycle, then create Cart
and CartItem
as structs and have CartItem
use Product as simple property:
struct CartItem {
let product: Product
var quantity: Int
}
You will have to use CartItem
in the main thread to display your UI so this will work great in practice where main MOC will hold your Product
instances.
Do not over-complicate your model. I don’t use abstract entities, parent entities, ordered relationships, stored fetched properties and what not. If you need something like that, Core Data is not the answer I would recommend even though it has such features.
Denormalize freely. Core Data constructs like MOC and FRC are working the best when they deal with just one Entity at the time. Thus don’t hesitate to repeat bits of data as attributes across various entities in the model.
Automate class file generation
I am still using mogenerator out of habit, despite the project seemingly being abandoned.
I am using my own set of Swift templates (see brief summary in the PR) and a custom build of mogenerator to run them.
I will probably look into switching to SwiftGen as the templates will likely be simpler to maintain. At least that project is still going strong.
Use Operation(Queue) for writes
Keeping Core Data entity relationships consistent is the most crucial and fragile point. I have solved this by using OperationQueue set-up like this:
final class DataManager {
private var processingQueue = OperationQueue()
init() {
setupProcessingQueue()
}
private func setupProcessingQueue() {
processingQueue.maxConcurrentOperationCount = 1
processingQueue.qualityOfService = .userInitiated
}
}
Then I have a simple AsyncOperation subclass which will do data processing on the background thread and perform the save at the end.
self.processingQueue.addOperation( SaveOperation(name: "SOME_NAME") {
[unowned self] in
let moc = self.coreDataStack.importerContext()
...
self.save(moc: moc)
}
This is the trickiest part and while it seems like a bottleneck, in practice it works amazingly well, even for continuous imports from something like a streaming web service.
RTSwiftCoreDataStack
Not so humble brag: my library is the best Core Data stack you will find anywhere. It has everything that NSPersistentContainer offers and adds so much more. Works in iOS 9, probably would work in iOS 8 too although I haven’t tried in years.
It gives you two PersistentStoreCoordinators out of the box: one regular and one aimed at large background data imports (see previous point about SaveOperations)
It has viewContext
declared in main thread and connected to main PSC which you can use for all your UI needs. Has a property to setup this MOC as read-only. Of course, you can freely use it create additional MOCs you may need.
The main power lies with the background imports using SaveOperation approach. The write-ahead feature of modern SQLite 3 versions makes sure that you never, ever experience bottlenecks with any of your reads while the writes stay performant.
The library makes sure to automatically merge any changes from the background MOCs into the existing objects in the main MOC.
Use Swift to the max
I wrote my own mogenerator templates because I wanted to use all the Swift benefits. See recommendations and improvements explained by:
- Brian King
- Trevor Squires
- yours truly here and here
thus I’ll skip writing more about it here. See also articles by Jesse Squires and Jordan Morgan for further improvement ideas.
Swift is far more usable language for Core Data modeling than Objective-C ever was. Just look at the true gem in my library – ManagedObjectType protocol. When applied to model classes, it gives you powerful yet simple to use methods to perform fetches, create strongly-typed FRCs, perform memory-only objects searches in any given MOC etc. I picked up initial version of this protocol from wonderful Core Data book by objc.io and extended it further to suit my needs.
I often get asked why am I not using Realm or some other way of persistence when Core Data is so complex, thread-unsafe and what not. Simple answer is that I haven’t yet encountered anything that I could not achieve with Core Data. I have not hit a wall which would force me to look elsewhere.
I know Core Data. I can use its strengths and I know how to handle its pitfalls. It’s heavily supported and actively developed by platform vendor and that counts a lot.
Ignore the FUD and make your own judgement. Don’t be afraid to forge your own path; don’t just blindly follow recommendations from any authority figure in the community. Core Data is mature and large enough that you can experiment and find the best approach for your specific app needs.