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?
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.
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).
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.
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)
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 NSImage
s. 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:
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
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).
Codable
is fundamentally more limited than NSCoding
. Codable
is implemented in a way that can only handle objects with value semantics. As a result:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With