Reputation: 173
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
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 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.
Upvotes: 8
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