Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class conforming to Codable protocol fails with encodeWithCoder: unrecognized selector sent to instance

I know that there are several questions similar to this, that tend to all revolve around the class not conforming to the protocol properly, but that should not be the immediate issue here.

The following is a condensed version of the code that is currently giving me this problem:

enum Binary: Int {
    case a = 0
    case b = 1
    case c = 9
}

final class MyClass: NSCoder {
    var string: String?
    var date: Date?
    var binary: Binary = .c

    override init() { }

    enum CodingKeys: CodingKey {
        case string, date, binary
    }
}

extension MyClass: Codable {
    convenience init(from decoder: Decoder) throws {
        self.init()

        let values = try decoder.container(keyedBy: CodingKeys.self)
        string = try values.decode(String.self, forKey: .string)
        date = try values.decode(Date.self, forKey: .date)
        binary = try Binary(rawValue: values.decode(Int.self, forKey: .binary)) ?? .c
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(string, forKey: .string)
        try container.encode(date, forKey: .date)
        try container.encode(binary.rawValue, forKey: .binary)
    }
}

I have created the following class which then attempts to call MyClass with the purpose of writing & reading it to UserDefaults:

class MyClassController {
    private let myClass: MyClass

    init() {
        self.myClass = MyClass()
        self.myClass.string = "string"
        self.myClass.date = Date()
        self.myClass.binary = .a
    }

    func writeMyClass() {
        let encodedData = NSKeyedArchiver.archivedData(withRootObject: myClass)
        UserDefaults.standard.set(encodedData, forKey: String(describing: MyClass.self))
    }

    func readMyClass() {
        if let decoded = UserDefaults.standard.object(forKey: String(describing: MyClass.self)) as? Data,
            let myClass = NSKeyedUnarchiver.unarchiveObject(with: decoded as Data) as? MyClass {
            print("string: \(myClass.string ?? "nil") date: \(myClass.date ?? Date()) binary: \(myClass.binary)")
        }
    }
}

As soon as I call the writeMyClass function though, I get this error:

[DemoDecoder.MyClass encodeWithCoder:]: unrecognized selector sent to instance #blahblah#

Two things I have also tried:

  • Adding func encode(with aCoder: NSCoder) to MyClass
  • Removed all properties from MyClass & CodingKeys and the init/encode functions
like image 585
CodeBender Avatar asked Dec 05 '17 17:12

CodeBender


1 Answers

You have a lot of mismatched attempts and various encoding/decoding mechanisms.

NSKeyedArchiver and NSKeyedUnarchiver require that all involved types conform to the NSCoding protocol. This is the older mechanism from the Objective-C frameworks.

The protocols Codable, Encoder, and Decoder are new to Swift 4. Such data types should be used with Swift encoder and decoders such as JSONEncoder and JSONDecoder or PropertyListEncoder and PropertyListDecoder.

I suggest you remove the reference to NSCoder and remove the uses of NSKeyedArchiver and NSKeyedUnarchiver. Since you have implemented the Codable protocol, use an appropriate Swift encoder and decoder. In your case you want to use PropertyListEncoder and PropertyListDecoder.

Once that is done you should probably change MyClass to be a struct instead of a class.

You should also avoid use UserDefaults to store data. Write the encoded data to a plist file instead.

like image 114
rmaddy Avatar answered Sep 25 '22 14:09

rmaddy