Can NSCoding and Codable co-exist?
Asked Answered
S

2

5

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?

Secessionist answered 7/6, 2018 at 16:57 Comment(0)
M
6

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)
Matlick answered 8/6, 2018 at 5:46 Comment(4)
Thanks for the answer. What if having the internal structure (Unward) as a structure is a requirement ? Sometimes you want Value types & copying. . .Secessionist
If you must have a Swift struct then you can't use NSCoding. You should avoid NSCoding and NSObject if at all possible. The only reason to use those is if you have no choice due to compatibility with Objective-C code.Matlick
I am not seeing where Codable is in play here any more. Once you go to NSCoding, you no longer get the benefits of Codable, correct? That is you have write the methods to conform. Or am I missing something?Nolte
@Nolte It would be pretty rare to support both Codable and NSCoding but the code in my answer does support both as written. There's nothing to write for Codable if you don't need any special processing, unlike NSCoding that requires code for everything.Matlick
A
8

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.

Arbitrator answered 26/2, 2022 at 19:21 Comment(4)
How does this perform with 5000x5000 png NSImage? I found Codable to be ultra slow. Even worse with ".deferredToData". 4 seconds vs 16 seconds on M1 developer.apple.com/forums/thread/105952Klingensmith
@MarekH No idea. Report back what you find.Arbitrator
I have tested that having large blobs of data with Codable is bad. Apple engineer confirmed it in the attached link.Klingensmith
@MarekH Yeah, that makes sense, especially the most common target for Codable is JSON, which is particularly inefficient at encoding binary data.Arbitrator
M
6

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)
Matlick answered 8/6, 2018 at 5:46 Comment(4)
Thanks for the answer. What if having the internal structure (Unward) as a structure is a requirement ? Sometimes you want Value types & copying. . .Secessionist
If you must have a Swift struct then you can't use NSCoding. You should avoid NSCoding and NSObject if at all possible. The only reason to use those is if you have no choice due to compatibility with Objective-C code.Matlick
I am not seeing where Codable is in play here any more. Once you go to NSCoding, you no longer get the benefits of Codable, correct? That is you have write the methods to conform. Or am I missing something?Nolte
@Nolte It would be pretty rare to support both Codable and NSCoding but the code in my answer does support both as written. There's nothing to write for Codable if you don't need any special processing, unlike NSCoding that requires code for everything.Matlick

© 2022 - 2025 — McMap. All rights reserved.