0
votes

I am wrapping Core Data objects into structs to make them Codable.

[NB: Before your direct me to writing the swift file for each Core Data class, I would like to say that wrapping the NSManagedObject children result from a conscious choice in favour of code maintainability, as the data model may evolve in the future.]

I have the few classes like these, here is an example:

struct CodableNeed : Codable {
    enum CodingKeys: String, CodingKey {
        ...
    }

    var need:Need

    init (_ need:Need) {
        self.need = need
    }

    init(from decoder: Decoder) throws {
        ....
    }

    func encode(to encoder: Encoder) throws {
        ....
    }
}

This works actually pretty well, as any update in the init(from:decoder) of the struct is actually stored in the ManagedObjectContext.

In order to let each NSManagedObject class instance return their own struct, I defined a protocol where each class instance may return their own Codable struct:

protocol CodableWhenWrapped {
    func wrapToCodable() -> Codable
}

extension Need : CodableWhenWrapped {

    func wrapToCodable() -> Codable {
        return CodableNeed(self)
    }
}

I then use this in an encoding function:

func jsonDataOfCodable<T:Encodable>(_ object:T) throws -> Data {
    let encoder = JSONEncoder()
    let data = try encoder.encode(object)
    return data
}

and I call this function to generate an URLSessionUploadTask :

func updateTaskFor<T: NSManagedObject> (_ object:T, withSession session:URLSession) throws -> URLSessionUploadTask
    where T: CodableWhenWrapped
{
    let encoder = JSONEncoder() 

    // Here is the compile error:  
    // " Cannot invoke 'jsonDataOfCodable' with an argument list of type '(Codable)' "
    let jsonData = try jsonDataOfCodable(object.wrapToCodable())

    // then continue with generating the uploadTask        
    let url = "https://myurl.com/"
    let request = URLRequest(url: url)
    let updateTask = session.uploadTask(with: request, from: jsonData) { (data, response, error) in
        ....
    }
}

Here is the issue: the code does not compile when calling jsonDataOfCodable : Cannot invoke 'jsonDataOfCodable' with an argument list of type '(Codable)'.

Any idea why the compiler does not like this?

Note that I have the same issue when I specify <T:Codable>instead of <T:Encodable> in the jsonDataOfCodable prototype.

1
JSONEncoder().encode requires a concrete type conforming to (En)codable, not the protocol Codable itself. You could use an associatedtype. It might be easier to implement init(from decoder: and encode(to encoder: in each NSManagedObject sublcass. Especially if there are relationships it's the only choice. - vadian

1 Answers

1
votes

I am answering my own question after reflecting on vadian's comment.

I solved the issue by updating my protocol:

protocol CodableWhenWrapped : Encodable {
    func wrapToCodable<T>() -> CodableWrapper<T>
    func update(from decoder:Decoder) throws
}

Then I added a Generic wrapper for my objects (all inheriting from a Synchronizable class):

struct CodableWrapper<T> : Codable
where T:CodableWhenWrapped, T: Synchronizable
{
    enum SynchronizableCodingKeys: String, CodingKey {
        case pk = "idOnServer"
        case lastModificationDate = "modified_on"
    }

    var object:T

    init(_ obj:T){
        self.object = obj
    }

    init(from decoder: Decoder) throws {
        // This is how the NSManagedObjectContext is passed through the decoder
        guard let codingUserInfoKeyManagedObjectContext = CodingUserInfoKey.managedObjectContext,
            let managedObjectContext = decoder.userInfo[codingUserInfoKeyManagedObjectContext] as? NSManagedObjectContext
            else {
                fatalError("Failed to decode Need: could not retriever the NSManagedObjectContext. Was it included in Decoder.userInfo as CodingUserInfoKey.managedObjectContext ?")
            }

        // check if there is a an existing object with the same idOnServer. If not, create a new one        
        let container = try decoder.container(keyedBy: SynchronizableCodingKeys.self)
        let pk = try container.decode(UUID.self, forKey: .pk)

        object = try Synchronizable.withIDOnServer(pk.uuidString, inMOC: managedObjectContext) ?? T(context: managedObjectContext)
        try object.update(from: decoder)
    }

    func encode(to encoder: Encoder) throws {
        try object.encode(to: encoder)
    }
}

I can then have a generic function to serialize to JSON:

func jsonDataFor<T>(_ object:T) throws -> Data
    where T:CodableWhenWrapped, T:Synchronizable {
    let encoder = JSONEncoder()
    let wrappedObject:CodableWrapper<T> = object.wrapToCodable()
    let jsonData = try encoder.encode(wrappedObject)
    return jsonData
}

Decoding is just:

let _ = try decoder.decode(CodableWrapper<T>.self, from: data!)

Thanks @vadian for your help, I hope this is useful for others! BTW, the CodableWhenWrapped protocol can easilt be reused for other objects types.