swiftswift4nscodingcodable

Can NSCoding and Codable co-exist?


In testing how the new Codable interacts with NSCoding I have put together a playground test involving an NSCoding using Class that contains a Codable structure. To whit

struct Unward: Codable {
    var id: Int
    var job: String
}

class Akward: NSObject, NSCoding {

    var name: String
    var more: Unward

    init(name: String, more: Unward) {
        self.name = name
        self.more = more
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(more, forKey: "more")
    }

    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
        more = aDecoder.decodeObject(forKey: "more") as? Unward ?? Unward(id: -1, job: "unk")
        super.init()
    }
}

var upone = Unward(id: 12, job: "testing")
var adone = Akward(name: "Adrian", more: upone)

The above is accepted by the Playground and does not generate any complier errors.

If, however, I try out Saving adone, as so:

let encodeit = NSKeyedArchiver.archivedData(withRootObject: adone)

The playground promptly crashes with the error:

error: Execution was interrupted, reason: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0).

Why? Is there any way to have an NSCoding class contain a Codable structure?


Solution

  • The actual error you are getting is:

    -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance

    And this is coming from the line:

    aCoder.encode(more, forKey: "more")
    

    The cause of the problem is that more (of type Unward) doesn't conform to NSCoding. But a Swift struct can't conform to NSCoding. You need to change Unward to be a class that extends NSObject in addition to conforming to NSCoding. None of this affects the ability to conform to Codable.

    Here's your updated classes:

    class Unward: NSObject, Codable, NSCoding {
        var id: Int
        var job: String
    
        init(id: Int, job: String) {
            self.id = id
            self.job = job
        }
    
        func encode(with aCoder: NSCoder) {
            aCoder.encode(id, forKey: "id")
            aCoder.encode(job, forKey: "job")
        }
    
        required init?(coder aDecoder: NSCoder) {
            id = aDecoder.decodeInteger(forKey: "id")
            job = aDecoder.decodeObject(forKey: "job") as? String ?? ""
        }
    }
    
    class Akward: NSObject, Codable, NSCoding {
        var name: String
        var more: Unward
    
        init(name: String, more: Unward) {
            self.name = name
            self.more = more
        }
    
        func encode(with aCoder: NSCoder) {
            aCoder.encode(name, forKey: "name")
            aCoder.encode(more, forKey: "more")
        }
    
        required init?(coder aDecoder: NSCoder) {
            name = aDecoder.decodeObject(forKey: "name") as? String ?? ""
            more = aDecoder.decodeObject(forKey: "more") as? Unward ?? Unward(id: -1, job: "unk")
        }
    }
    

    And your test values:

    var upone = Unward(id: 12, job: "testing")
    var adone = Akward(name: "Adrian", more: upone)
    

    You can now archive and unarchive:

    let encodeit = NSKeyedArchiver.archivedData(withRootObject: adone)
    let redone = NSKeyedUnarchiver.unarchiveObject(with: encodeit) as! Akward
    

    And you can encode and decode:

    let enc = try! JSONEncoder().encode(adone)
    let dec = try! JSONDecoder().decode(Akward.self, from: enc)