Code Monkey home page Code Monkey logo

coredatable's Introduction

Coredatable

Easy Codable conformance in NSManagedObject subclasses.

This library is in beta phase. Until version 1.0.0 is released, breaking changes might be added.

Installation

CocoaPods

Add the following to your Podfile:

pod 'Coredatable'

Summary

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 do typealias CodingKeys = CoreDataDefaultCodingKeys.
  • If you want to use a different set of keys, you must create a enum called CodingKeys, make it conforms CoreDataCodingKeys 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.

Identity Attributes.

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.

Serializing relationships from identifiers

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.

KeyPath Coding Keys

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 { "->" }
}

Custom Decoding

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: Overreding func initialize(from container: CoreDataKeyedDecodingContainer<Custom>)
  • // 2: Call the default serialization only taking in account the id 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 firstandsecond` values from the container.
  • // 5: Adding the joined string to the compound 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: Override static 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

Many

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.

Inspiration:

This library has been heavily inspired by Groot

Author

@ManueGE

License

Coredatable is available under the MIT License. See LICENSE.

coredatable's People

Contributors

manuege avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

nik3212

coredatable's Issues

Doesn’t encode object with NSSet? property

I'm trying to encode the following model:

final class ManagedHabit: NSManagedObject, CoreDataCodable {
    @NSManaged var id: String
    @NSManaged var name: String
    @NSManaged var notifications: NSSet?
    
    enum CodingKeys: String, CoreDataCodingKey {
        case id
        case name
        case notifications
    }
}

final class ManagedNotification: NSManagedObject, CoreDataCodable {
    @NSManaged var id: String
    @NSManaged var userNotificationId: String
    @NSManaged var enabled: Bool
    @NSManaged var habit: ManagedHabit
    
    enum CodingKeys: String, CoreDataCodingKey {
        case id
	case userNotificationId
	case enabled
    }
}

…

let encoder = JSONEncoder()
let jsonData = try encoder.encode(object)

I get an error.

The CoreDataEncoder class contains a method

private func encode (_ relationship: NSRelationshipDescription, object: ManagedObject, key: Keys, container: KeyedContainer) throws

that uses NSStringFromClass in its implementation, which always returns nil. This is because you need to use NSClassFromString(“<ModuleName>.<ClassName>”). I checked this and it works.

The same problem is present in the CoreDataDecodable extension in the method

private func set (_ relationship: NSRelationshipDescription, from container: CoreDataKeyedDecodingContainer <Self>, with codingKey: CodingKeys) throws

Please, can you fix this?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.