ArtbyKGH
ArtbyKGH

Reputation: 173

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?

Upvotes: 6

Views: 5879

Answers (2)

Alexander
Alexander

Reputation: 63157

The existing answer doesn't really address the question of interop, rather, it shows how to migrate from NSCoding to Codable.

I had a use-case where this wasn't an option, and I did genuinely need to use NSCoding from a Codable context. In case you're curious: I needed to send models between XPC services of my Mac app, and those models contained NSImages. I could have made a bunch of DTOs that serialize/deserialize the images, but that would be a lot of boiler plate. Besides, this is a perfect use case for property wrappers.

Here's the property wrapper I came up with:

@propertyWrapper
struct CodableViaNSCoding<T: NSObject & NSCoding>: Codable {
    struct FailedToUnarchive: Error { }

    let wrappedValue: T

    init(wrappedValue: T) { self.wrappedValue = wrappedValue }

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)

        let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
        unarchiver.requiresSecureCoding = Self.wrappedValueSupportsSecureCoding

        guard let wrappedValue = T(coder: unarchiver) else {
            throw FailedToUnarchive()
        }

        unarchiver.finishDecoding()

        self.init(wrappedValue: wrappedValue)
    }

    func encode(to encoder: Encoder) throws {
        let archiver = NSKeyedArchiver(requiringSecureCoding: Self.wrappedValueSupportsSecureCoding)
        wrappedValue.encode(with: archiver)
        archiver.finishEncoding()
        let data = archiver.encodedData

        var container = encoder.singleValueContainer()
        try container.encode(data)
    }

    private static var wrappedValueSupportsSecureCoding: Bool {
        (T.self as? NSSecureCoding.Type)?.supportsSecureCoding ?? false
    }
}

And here are the simple test I wrote for it:

import Quick
import Nimble

import Foundation

@objc(FooTests_SampleNSCodingClass)
private class SampleNSCodingClass: NSObject, NSCoding {
    let a, b, c: Int

    init(a: Int, b: Int, c: Int) {
        self.a = a
        self.b = b
        self.c = c
    }

    required convenience init?(coder: NSCoder) {
        self.init(
            a: coder.decodeInteger(forKey: "a"),
            b: coder.decodeInteger(forKey: "b"),
            c: coder.decodeInteger(forKey: "c")
        )
    }

    func encode(with coder: NSCoder) {
        coder.encode(a, forKey: "a")
        coder.encode(b, forKey: "b")
        coder.encode(c, forKey: "c")
    }
}

@objc(FooTests_SampleNSSecureCodingClass)
private class SampleNSSecureCodingClass: SampleNSCodingClass, NSSecureCoding {
    static var supportsSecureCoding: Bool { true }
}

private struct S<T: NSObject & NSCoding>: Codable {
    @CodableViaNSCoding
    var sampleNSCodingObject: T
}

class CodableViaNSCodingSpec: QuickSpec {
    override func spec() {
        context("Used with a NSCoding value") {
            let input = S(sampleNSCodingObject: SampleNSCodingClass(a: 123, b: 456, c: 789))

            it("round-trips correctly") {
                let encoded = try JSONEncoder().encode(input)

                let result = try JSONDecoder().decode(S<SampleNSCodingClass>.self, from: encoded)

                expect(result.sampleNSCodingObject.a) == 123
                expect(result.sampleNSCodingObject.b) == 456
                expect(result.sampleNSCodingObject.c) == 789
            }
        }

        context("Used with a NSSecureCoding value") {
            let input = S(sampleNSCodingObject: SampleNSSecureCodingClass(a: 123, b: 456, c: 789))

            it("round-trips correctly") {
                let encoded = try JSONEncoder().encode(input)

                let result = try JSONDecoder().decode(S<SampleNSSecureCodingClass>.self, from: encoded)

                expect(result.sampleNSCodingObject.a) == 123
                expect(result.sampleNSCodingObject.b) == 456
                expect(result.sampleNSCodingObject.c) == 789
            }
        }
    }
}

A few notes:

  1. If you need to go the other way (embed Codable objects inside an NSCoding archive), you can use the existing methods that were added to NSCoder/NSDecoder

  2. This is creating a new archive for every object. In addition to adding quite a few object allocations during encoding/decoding, it also might bloat the result (it was around 220 bytes for an empty archive, in my testing).

  3. Codable is fundamentally more limited than NSCoding. Codable is implemented in a way that can only handle objects with value semantics. As a result:

    • Object graphs that have aliases (multiple references to the same object) will cause those objected to be duplicated
    • Object graphs with cycles can never be decoded (there would be infinite recursion)

    This means that you can't really make a Encoder/Decoder wrapper around NSCoder/NSCoder classes (like NSKeyedArchiver/NSKeyedUnarchiver), without needing to put in a bunch bookkeeping to detect these scenarios and fatalError. (It also means you can't support archiving/unarchiving any general NSCoding object, but only those with no aliases or cycles). This is why I went with the "make a standalone archive and encode it as Data" appoach.

Upvotes: 8

rmaddy
rmaddy

Reputation: 318774

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)

Upvotes: 6

Related Questions