Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 383
ArtbyKGH Avatar asked Jun 07 '18 16:06

ArtbyKGH


People also ask

Can a class conform to Codable?

If all the properties of a type already conform to Codable , then the type itself can conform to Codable with no extra work – Swift will synthesize the code required to archive and unarchive your type as needed.

What is NSCoding in Swift?

The NSCoding protocol declares the two methods that a class must implement so that instances of that class can be encoded and decoded. This capability provides the basis for archiving (where objects and other structures are stored on disk) and distribution (where objects are copied to different address spaces).

What is Swift Codable?

Codable allows you to insert an additional clarifying stage into the process of decoding data into a Swift object. This stage is the “parsed object,” whose properties and keys match up directly to the data, but whose types have been decoded into Swift objects.


2 Answers

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)
like image 98
rmaddy Avatar answered Sep 28 '22 07:09

rmaddy


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.

like image 32
Alexander Avatar answered Sep 28 '22 07:09

Alexander