Easy Codable
conformance in NSManagedObject
subclasses.
This library is in beta phase. Until version
1.0.0
is released, breaking changes might be added.
Add the following to your Podfile
:
pod 'Coredatable'
Adding Decodable
and Encodable
conformance to NSManagedObject
subclasses is usually very tricky. Coredatable
simplifies this process using equivalent protocols called CoreDataDecodable
, CoreDataEncodable
and CoreDataCodable
this way:
final class Person: NSManagedObject, CoreDataCodable, UsingDefaultCodingKeys {
@NSManaged var id: Int
@NSManaged var name: String?
@NSManaged var city: String?
@NSManaged var birthday: Date?
@NSManaged var attributes: NSSet
}
class PersonAttribute: NSManagedObject, CoreDataCodable {
@NSManaged private(set) var id: Int
@NSManaged private(set) var name: String
enum CodingKeys: String, CoreDataCodingKey {
case id
case name = "attributeName"
}
}
// Decode
let decoder = JSONDecoder()
decoder.managedObjectContext = myContext
let person = try decoder.decode(Person.self, from: data)
// Encode
let encoder = JSONEncoder()
let data = try encoder.encode(person)
And yes, that's all.
You just need to add a NSManagedObjectContext
to your decoder, and make the classes conform the protocol. The protocol forces you to add a CodingKeys
type. In the samples we have two different cases:
- If the keys to use are the default ones (same names as the property) you can just conform
UsingDefaultCodingKeys
. Alternatively, you could dotypealias CodingKeys = CoreDataDefaultCodingKeys
. - If you want to use a different set of keys, you must create a
enum
calledCodingKeys
, make it conformsCoreDataCodingKeys
and define the cases and its string values.
In the case you want a more customized CodingKey
, you can create a class
a struct
or any other type and make it conforms AnyCoreDataCodingKey
.
Optionally, you can ensure uniqueness in your NSManagedObject
instances. To do it, you can use identityAttribute
property:
final class Person: NSManagedObject, CoreDataCodable, UsingDefaultCodingKeys {
@NSManaged var id: Int
@NSManaged var name: String?
@NSManaged var country: String?
@NSManaged var birthday: Date?
@NSManaged var attributes: NSSet
static let identityAttribute: IdentityAttribute = #keyPath(Person.id)
}
If you do this, Coredatable
will take care of check if an object with the same value for the identityAttribute
already exists in the context. If it exists, the object will be updated with the new values. If it doesn't, it will be just inserted. When an object is updated, only the values present in the new json are updated.
Composite identity attributes are supported this way:
static let identityAttribute: IdentityAttribute = [#keyPath(Person.id), #keyPath(Person.name)]
However, only use composite identity attributes if it is really needed because the performance will be affected. The single identity attribute strategy requires one fetch for every array of JSON objects, whereas the composite identity attribute strategy requires one fetch for every single JSON object.
If uniqueness is not required, you can exclude identityAttribute
at all.
Suppose that our API does not return full objects for the relationships but only the identifiers.
We don't need to change our model to support this situation:
let json: [String: Any] = [
"id": "1",
"name": "Marco",
"attributes": [1, 2]
]
let data = try JSONSerialization.data(withJSONObject: json, options: [])
let person = try decoder.decode(Person.self, from: data)
The above code creates a Person
object. In the case we already have in our context a PersonAttribute
with ids 1
or 2
, those objects will be set in the relationship. If not, two PersonAttribute
instances will be created with the given id
.
Note that serializing relationships from identifiers only works with entities specifying only one attribute as the value of
identityAttribute
.
Let's supose we have a value which is inside a nested json:
{
"id": 1,
"name": "Marco",
"origin": {
"country" {
"id": 1,
"name": "Spain"
}
}
}
we can access the country name directly using key parh notation:
enum CodingKeys: String, CoreDataCodingKey {
case id, name, birthday, attributes
case country = "origin.country.name"
}
by default, keypaths use a period .
as paths delimiter, but you could use any other string sdding this:
enum CodingKeys: String, CoreDataCodingKey {
case id, name, birthday, attributes
case country = "origin->country->name"
var keyPathDelimiter: String { "->" }
}
In the case that you need custom serialization, you'll need to do something slightly different from what you'd do in regular Codable
. Instead of overriding init(from decoder: Decoder)
you should override func initialize(from decoder: Decoder) throws
.
Let's see an example:
final class Custom: NSManagedObject, CoreDataDecodable {
@NSManaged var id: Int
@NSManaged var compound: String
enum CodingKeys: String, CoreDataCodingKey {
case id
case first
case second
}
static var identityAttribute: IdentityAttribute = #keyPath(Custom.id)
// 1
func initialize(from decoder: Decoder) throws {
// 2
try defaultInitialization(from: decoder, with: [.id])
// 3
let container = try decoder.container(for: Custom.self)
// 4
let first = try container.decode(String.self, forKey: .first)
let second = try container.decode(String.self, forKey: .second)
// 5
compound = [first, second].joined(separator: " ")
}
}
here, we have a propery compound
which is built joining two strings which come under different keys. What we do is:
// 1
: Overredingfunc initialize(from container: CoreDataKeyedDecodingContainer<Custom>)
// 2
: Call the default serialization only taking in account theid
key. (Note: there are a few more default implementations where you can specify what keys are included or skipped)// 3
: Create a new container passing the own type as parameter. Passing another type will lead to errors.// 4: Extract the
firstand
second` values from the container.// 5
: Adding the joined string to thecompound
property.
If you need to perform some changes in the identityAttributes
before the json is serialized, you need to use a different method. Let's supose that our json sends id
as a String
but we have it as an integer. We can modifiy the value using:
// 1
static func container(for decoder: Decoder) throws -> AnyCoreDataKeyedDecodingContainer {
// 2
var container = try decoder.container(for: CustomDoubleId.self)
// 3
container[.id] = Int(try container.decode(String.self, forKey: .id)) ?? 0
// 4
return container
}
// 1
: Overridestatic func container(for decoder: Decoder) throws -> AnyCoreDataKeyedDecodingContainer
// 2
: Create a new mutable container passing the own type as parameter. Passing another type will lead to errors.// 3
: Convert the value to the needed one and assing it to the.id
key// 4
: return the modified container
You can use CoreDataDecodable
objects nested in another Codable
object without any problem:
struct LoginResponse: Codable {
let token: String
let user: Person
}
However, in the case you want to reference an array of CoreDataDecodable
objects:
struct Response: Codable {
let nextPage: String
let previousPage: String
let total: Int
// don't do this
let results: [Person]
}
it is better if you use Many
instead of Array
:
struct Response: Codable {
let nextPage: String
let previousPage: String
let total: Int
let results: Many<Person>
}
Using Many
will improve performance. Many
is a replacement of Array
and can be used in the same way. In any case, you can access the raw array using many.array
.
This library has been heavily inspired by Groot
Coredatable is available under the MIT License. See LICENSE.