Search code examples
swiftnsattributedstringnskeyedarchiver

Cannot invoke 'encode' with an argument list of type 'NSAttributedString'


How can I encode a NSAttributedString and store it in disk using UserDefaults?

First I tried setting the Codable protocol:

class CoachProgram: Codable {
    var name: String
    var details: NSAttributedString
}

but the compiler yields Type 'Coach' does not conform to protocol 'Encodable'. So, I started to implement the encode(to:) method:

public func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    try container.encode(details)
                  ^~~~~~ "error: cannot invoke 'encode' with an argument list of type '(NSAttributedString)'"
}

but no success.

My idea is to use the CoachProgram like:

let archiver = NSKeyedArchiver()
do {
    try archiver.encodeEncodable(coachProgram, forKey: NSKeyedArchiveRootObjectKey)
}
catch {
    print(error)
}
UserDefaults.standard.set(archiver.encodedData, forKey: "CoachProgram")

Solution

  • First problem: I was trying to use a Unkeyed Container and that doesn't makes sense because I definitely need a Keyed Container because I have 2 attributes (name and details).

    So, I needed to implement some CodingKey's and use decoder.container(keyedBy:) method.

    Second problem: after some experiments, I noticed I could turn a NSAttributedString into Data by simple using the NSKeyedArchiver! The Data is Codable so I can encode and decode it.

    So, the final solution I got:

    class CoachProgram: Codable {
        var name: String
        var details: NSAttributedString
    
        enum CodingKeys: String, CodingKey {
            case name
            case details
        }
    
        init(name: String, details: NSAttributedString) {
            self.name = name
            self.details = details
        }
    
        required public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            if let name = try container.decodeIfPresent(String.self, forKey: .name) {
                self.name = name
            }
            if let data = try container.decodeIfPresent(Data.self, forKey: .details) {
                self.details = NSKeyedUnarchiver.unarchiveObject(with: data) as? NSAttributedString ?? NSAttributedString()
            }
        }
    
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(name, forKey: .name)
            try container.encode(NSKeyedArchiver.archivedData(withRootObject: details), forKey: .details)
        }
    }
    

    In action:

    let coachProgram = CoachProgram(name: "John Doe", details: NSAttributedString())
    
    let archiver = NSKeyedArchiver()
    do {
        try archiver.encodeEncodable(coachProgram, forKey: NSKeyedArchiveRootObjectKey)
    }
    catch {
        print(error)
    }
    UserDefaults.standard.set(archiver.encodedData, forKey: "CoachProgram")