符合 Codable 协议的类因 encodeWithCoder 失败:无法识别的选择器发送到实例

Posted

技术标签:

【中文标题】符合 Codable 协议的类因 encodeWithCoder 失败:无法识别的选择器发送到实例【英文标题】:Class conforming to Codable protocol fails with encodeWithCoder: unrecognized selector sent to instance 【发布时间】:2018-05-19 10:30:54 【问题描述】:

我知道有几个与此类似的问题,往往都围绕着没有正确遵守协议的类,但这不应该是这里的直接问题。

以下是当前给我这个问题的代码的精简版本:

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)
    

我创建了以下类,然后尝试调用MyClass,目的是将其写入和读取到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)")
        
    

不过,只要我调用 writeMyClass 函数,我就会收到此错误:

[DemoDecoder.MyClass encodeWithCoder:]:无法识别的选择器发送到 实例#blahblah#

我也尝试过两件事:

func encode(with aCoder: NSCoder) 添加到MyClassMyClassCodingKeys 以及 init/encode 函数中删除了所有属性

【问题讨论】:

【参考方案1】:

你有很多不匹配的尝试和各种编码/解码机制。

NSKeyedArchiverNSKeyedUnarchiver 要求所有涉及的类型都符合NSCoding 协议。这是来自 Objective-C 框架的旧机制。

CodableEncoderDecoder 协议是 Swift 4 的新协议。此类数据类型应与 Swift 编码器和解码器一起使用,例如 JSONEncoderJSONDecoderPropertyListEncoder 和 @987654330 @。

我建议您删除对NSCoder 的引用并删除对NSKeyedArchiverNSKeyedUnarchiver 的使用。由于您已经实现了Codable 协议,请使用适当的 Swift 编码器和解码器。在您的情况下,您想使用 PropertyListEncoderPropertyListDecoder

完成后,您可能应该将 MyClass 更改为 struct 而不是 class

您还应该避免使用UserDefaults 来存储数据。将编码数据写入 plist 文件。

【讨论】:

谢谢,我已经很多年没有使用这些了,并且对新协议和旧方法感到困惑。我将更新我的答案以包含工作代码。 @CodeBender 你还没有发布答案。并且不要使用工作代码更新您的问题。如果需要,请发布实际答案。将问题和答案分开。【参考方案2】:

这是从上面 rmaddy 提供的答案中得出的工作代码。

几个亮点:

    将 MyClass 转换为 MyStruct 从我希望保存的对象中删除了 NSCoder 继承 删除了对NSKeyedArchiverNSKeyedUnarchiver 的调用 不再保存到UserDefaults 依赖JSONEncoder&JSONDecoder写出struct 现在作为 Data 对象写入文件系统

这是我希望保存的更新后的结构和枚举:

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)
    

处理读取和写入输出的更新控制器类。就我而言,写出 JSON 很好,所以我采用了这种方法。

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)
        
    

【讨论】:

【参考方案3】:

@CodeBender 的解决方案工作得很好,虽然 没有必要使用 init(from decoder: Decoder)encode(to encoder: Encoder) 方法进行手动编码/解码,这样做只是违背了伟大的目的 可编码协议,除非您需要进行一些复杂级别的编码/解码。

下面是使用 Codable 协议的纯粹优势运行良好的代码:


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)
        
    


【讨论】:

以上是关于符合 Codable 协议的类因 encodeWithCoder 失败:无法识别的选择器发送到实例的主要内容,如果未能解决你的问题,请参考以下文章

Codable 类不符合协议 Decodable

如果某些东西符合 Codable ,它会永远无法被编码或解码吗?

Codable和CodingKeys

如何在目标 c 数据模型类中使用 Codable 协议?

忽略 Codable 对象中的非 Codable 可选属性

类型 '' 不符合协议 'Decodable'/'Encodable'