Codable CoreData
Swift 4, amongst other things, brought a way to serialize/deserialize data into/from model objects called Codable
. Those changes were proposed under SE-0166.
I'm not going to elaborate on how Codable
works since it's basically pretty straight forward. If you want to know more about it, I participated in a talk about which you can read more here (in German, though) or check out the sample repository on GitHub.
In this post I want to go into some details of using Codable
in combination with CoreData.
Sample structure
To make some things easier to understand, let's create a structure to use for this blog post:
final class Person: NSManagedObject {
@NSManaged var id: Int32
@NSManaged var firstName: String
@NSManaged var lastName: String
@NSManaged var birthday: Date
@NSManaged var dogs: Set<Dog>
}
final class Dog: NSManagedObject {
@NSManaged var id: Int32
@NSManaged var name: String
@NSManaged var master: Person
}
Encodable
As you probably know, Codable
is a combination of Encodable
and Decodable
. Using the former with NSManagedObject
subclasses is almost as straightforward as you might expect it to be. As long as the conditions are met (that mainly being the properties of your model being also Encodable
), you can just slap the Encodable
protocol to your NSManagedObject
subclass.
But if we have a closer look at the sample, we may notice that we might run into an infinite encoding loop. Why? Because CoreData generates relation properties on models. In our example the Person
has a Set<Dog>
and the Dog
again has a Person
. Trouble ahead!
Not to worry, though. Luckily we can just define our own CodingKeys and leave out the relation we don't want. In our case we don't want the Dog
to encode its master
again:
final class Dog: NSManagedObject, Encodable { ... } // -> Added `Encodable`
private extension Dog {
enum CodingKeys: String, CodingKey {
case id, name // -> no case for `master`
}
}
Now master
will not be encoded anymore and the loop is broken. Of course sometimes it's not that easy and you might have to implement the encode(to:)
yourself and dynamically decide whether or not to encode a relation.
But that's basically all that has to be said about encoding Core Data model classes.
Decodable
Now on to Decodable
. Here's where it gets tricky. Decodable
assumes, that your model can be created at anytime and only with the encoded data (no other dependencies). And we all know, that's not true for NSManagedObject
: it depends on NSManagedObjectContext
. There might be ways to insert an object later, but it still needs to know its entity description.
Another assumption Decodable
makes, is that it always creates objects, never updates existing ones. Together with the first assumption, this makes perfectly sense. Yet, since we already struggle with the first assumption, the second one makes things worse for NSManagedObject
subclasses. They're usually representing an entry in a database (usually SQLite). And we don't want to continually insert objects and have duplicates. Instead we want to update our existing objects.
While there might be ways to work around all these issues, its probably too much hassle. There might be APIs that allow creating NSManagedObject
s and instead of simply inserting them allowing them to update existing entries. There might be ways of making sure, that this also triggers the necessary changes so that existing (parent) contexts can be updated and eventually the new data can be displayed.
If you want to go down that road feel free! Once you succeed, be sure to let me know!
Meanwhile, let's look at another solution, that actually isn't even new: Data Transfer Objects - or in short DTOs!
Why don't we let the decoder create us a lightweight object which we can use to update our models? The downside of course is, that we have to write code for updating our models ourselves. But since Decodable
anyways can't update models (only create them), that's something we'd likely have to do anyways. Also, with all the aforementioned issues, we might have anyways had to implement init(from:)
ourselves, so we wouldn't have gotten much from the compiler anyways.
CoreDataDecodable
Alright, let's put that approach into some nice protocols:
protocol CoreDataDecodable: Decodable {
associatedType DTO: Decodable
@discardableResult
static func findOrCreate(for dto: DTO, in context: NSManagedObjectContext) throws -> Self
init(with dto: DTO, in context: NSManagedObjectContext) throws
mutating func update(from dto: DTO) throws
}
We create an associated type for our DTO (that of course needs to be Decodable
).
Next, we define a static func to allow us to search for an existing object for a given DTO inside a given NSManagedObjectContext
. If we don't find one, we simply create one.
The init(with:in:)
defines the initializer for creating object from DTOs in a given object context. And update(from:)
is for updating an existing model with a given DTO.
You might notice, that our protocol still inherits from Decodable
. Why so? Because then we still have the possibility to create objects directly from the decoder. But we don't want to methods to do that. We could create an extension, but as you might have noticed, our methods all require an NSManagedObjectContext
. So if we implement init(from:)
in an extension, we need to get a NSManagedObjectContext
from somewhere.
To solve this problem, we create a globally accessible context for decoding purposes:
enum CoreDataDecodingError: Error {
case missingContext(codingPath: [CodingKey])
}
extension NSManagedObjectContext {
private static var _decodingContext: NSManagedObjectContext?
static func decodingContext(at codingPath: [CodingKey] = []) throws -> NSManagedObjectContext {
if let context = _decodingContext { return context }
throw CoreDataDecodingError.missingContext(codingPath: codingPath)
}
public final func asDecodingContext(do work: () -> ()) {
NSManagedObjectContext._decodingContext = self
defer { NSManagedObjectContext._decodingContext = nil }
performAndWait { work() }
}
}
To make it easier to debug issues, we also pass down the codingPath
([CodingKey]
).
Note: If you need to decode from multiple threads (read queues), you cannot simply have one static property. You need to make sure, each thread (read queue) has its decoding context (e.g. using DispatchSpecificKey
). This is why the static var
is private
making it an implementation detail of decodingContext(at:)
.
Now that we've solved this problem, we can implement init(from:)
in an extension:
extension CoreDataDecodable {
init(from decoder: Decoder) throws {
try self.init(with: DTO(from: decoder), in: .decodingContext(at: decoder.codingPath))
}
}
If our protocol is implemented by an NSManagedObject (which is probably almost always the case), we can also give a default implementation of the initializer:
extension CoreDataDecodable where Self: NSManagedObject {
init(with dto: DTO, in context: NSManagedObjectContext) throws {
self.init(context: context)
try update(from: dto)
}
}
This leaves us with only two methods to implement: findOrCreate(for:in:)
and update(from:)
.
Example
Lets put that now into our example:
extension Person: CoreDataDecodable {
struct DTO: Decodable {
enum CodingKeys: String, CodingKey {
case id
case firstName = "first_name"
case lastName = "last_name"
case birthday, dogs
}
let id: Int32
let firstName: String
let lastName: String
let birthday: Date
let dogs: [Dog.DTO]
}
func update(from dto: DTO) throws {
id = dto.id
firstName = dto.firstName
lastName = dto.lastName
birthday = dto.birthday
guard let moc = managedObjectContext else { return }
dogs = try Set(dto.dogs.map { try Dog.findOrCreate(for: $0, in: moc) })
}
}
extension Dog: CoreDataDecodable {
struct DTO: Decodable {
let id: Int32
let name: String
}
func update(from dto: DTO) throws {
id = dto.id
name = dto.name
}
}
Note: You might have noticed the findOrCreate(for:in:)
is missing in the example. This is simply, because the logic is pretty straightforward. Using a NSFetchRequest
, we search for an object that (in our example) has the given id and update it if found. If not found, we create a new one using the dto
.
And that's it. Using this setup we're able to update existing models using `Codable`. Furthermore, we don't have to care about the implementation for the `Codable` methods, since the compiler generates them for us (either directly in the model for `Encodable` or in the `DTO` for `Decodable`).
FFCoreData
Since I already have a helper framework for CoreData usages, I've put this into the framework as well.
In that implementation, the findOrCreate(for:in:)
also has a default implementation, since the framework already has findOrCreate
logic.
You can find the repository on GitHub.