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

3

5

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
Extinctive answered 5/12, 2017 at 17:12 Comment(0)
A
7

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.

Alsatian answered 5/12, 2017 at 17:25 Comment(2)
Thanks, I have not worked with these in years and got mixed up with the new protocols & old approach. I will update my answer to include the working code.Extinctive
@Extinctive You haven't posted an answer. And do not update your question with working code. Post an actual answer if you want. Keep questions and answer separate.Alsatian
E
6

This is the working code derived from the answer provided by rmaddy above.

A few highlights:

  1. Convert MyClass to MyStruct
  2. Removed NSCoder inheritance from the object I wished to save
  3. Removed calls to NSKeyedArchiver & NSKeyedUnarchiver
  4. No longer saving to UserDefaults
  5. Relying on JSONEncoder & JSONDecoder to write out struct
  6. Writing to file system now as a Data object

This is the updated struct & enum that I wish to save:

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

struct MyStruct {
    var string: String?
    var date: Date?
    var binary: Binary = .c

    init() { }

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

extension MyStruct: Codable {
    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)
    }
}

The updated controller class that handles reading & writing the output. In my case, writing out to JSON was fine, so I went with that approach.

class MyStructController {
    private var myStruct: MyStruct

    init() {
        self.myStruct = MyStruct()
        self.myStruct.string = "string"
        self.myStruct.date = Date()
        self.myStruct.binary = .a
    }

    func writeMyStruct() {
        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(myStruct)
            let documentDirectory = try FileManager.default.url(for: .documentDirectory,
                                                                in: .userDomainMask,
                                                                appropriateFor:nil,
                                                                create:false)
            let url = documentDirectory.appendingPathComponent(String(describing: MyStruct.self))
            try data.write(to: url)
        } catch {
            print(error.localizedDescription)
        }
    }

    func readMyStruct() {
        do {
            let documentDirectory = try FileManager.default.url(for: .documentDirectory,
                                                                in: .userDomainMask,
                                                                appropriateFor:nil,
                                                                create:false)
            let url = documentDirectory.appendingPathComponent(String(describing: MyStruct.self))
            let data = try Data(contentsOf: url)
            let decoder = JSONDecoder()
            let myNewStruct = try decoder.decode(MyStruct.self, from: data)
            print("string: \(myNewStruct.string ?? "nil") date: \(myNewStruct.date ?? Date()) binary: \(myNewStruct.binary)")
        } catch {
            print(error.localizedDescription)
        }
    }
}
Extinctive answered 5/12, 2017 at 20:3 Comment(0)
C
0

Solution from @CodeBender works just fine, though there is no need to do manual encoding / decoding using init(from decoder: Decoder) and encode(to encoder: Encoder) methods, doing so just defeats the very purpose of the GREAT Codable protocol, unless you need to do some complex level of encoding / decoding.

Here is the code that works just well using the pure benefit of Codable protocol:


import UIKit

struct Movie: Codable {

    enum MovieGenere: String, Codable {
        case horror, drama, comedy, adventure, animation
    }

    var name : String
    var moviesGenere : [MovieGenere]
    var rating : Int
}

class MyViewController: UIViewController {


    override func viewDidLoad() {
        super.viewDidLoad()

        writeMyMovie(movie: Movie(name: "Titanic", moviesGenere: [Movie.MovieGenere.drama], rating: 1))

        readMyMovie()
    }

    var documentDirectoryURL:URL? {
        do {
            let documentDirectory = try FileManager.default.url(for: .documentDirectory,
                                                                in: .userDomainMask,
                                                                appropriateFor:nil,
                                                                create:false)
            return documentDirectory.appendingPathComponent(String(describing: Movie.self))
        } catch {
            return nil
        }
    }

    func writeMyMovie(movie:Movie) {

        do {
            let data = try JSONEncoder().encode(movie)
            try data.write(to: documentDirectoryURL!) // CAN USE GUARD STATEMENT HERE TO CHECK VALID URL INSTEAD OF FORCE UNWRAPPING, IN MY CASE AM 100% SURE, HENCE NOT GUARDING ;)
        } catch {
            print(error.localizedDescription)
        }
    }

    func readMyMovie() {
        do {
            let data = try Data(contentsOf: documentDirectoryURL!)
            let movie = try JSONDecoder().decode(Movie.self, from: data)
            print("MOVIE DECODED: \(movie.name)")
        } catch {
            print(error.localizedDescription)
        }
    }

}
Centurial answered 9/10, 2019 at 8:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.