1
votes

I need write and read NSAttributedString data into a json file, using this previously answered question I can encode the it but it throws an error while decoding.

class AttributedString : Codable {
    let attributedString : NSAttributedString

    init(attributedString : NSAttributedString) {
        self.attributedString = attributedString
    }

    public required init(from decoder: Decoder) throws {
        let singleContainer = try decoder.singleValueContainer()
        let base64String = try singleContainer.decode(String.self)
        guard let data = Data(base64Encoded: base64String) else { throw DecodingError.dataCorruptedError(in: singleContainer, debugDescription: "String is not a base64 encoded string") }
        guard let attributedString = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSAttributedString.self], from: data) as? NSAttributedString else { throw DecodingError.dataCorruptedError(in: singleContainer, debugDescription: "Data is not NSAttributedString") }
        self.attributedString = attributedString
    }

    func encode(to encoder: Encoder) throws {
        let data = try NSKeyedArchiver.archivedData(withRootObject: attributedString, requiringSecureCoding: false)
        var singleContainer = encoder.singleValueContainer()
        try singleContainer.encode(data.base64EncodedString())
    }
}

And:

do {
    let jsonEncoder = JSONEncoder()
    let jsonData = try jsonEncoder.encode(attributedString)
    let jsonString = String(data: jsonData, encoding: .utf8)
    print("***\n\(String(describing: jsonString))\n***") // It works
    let jsonDecoder = JSONDecoder()
    let attrib = try jsonDecoder.decode(AttributedString.self, from: jsonData)
    print(attrib.attributedString.string)

}catch{
    print(error) // throws error
}

Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NS.objects' was of unexpected class 'NSShadow'. Allowed classes are '{( NSGlyphInfo, UIColor, NSDictionary, UIFont, NSURL, NSParagraphStyle, NSString, NSAttributedString, NSArray, NSNumber )}'." UserInfo={NSDebugDescription=value for key 'NS.objects' was of unexpected class 'NSShadow'. Allowed classes are '{( NSGlyphInfo, UIColor, NSDictionary, UIFont, NSURL, NSParagraphStyle, NSString, NSAttributedString, NSArray, NSNumber )}'.}

PS: I need to keep attributes

1
try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? NSAttributedStringLeo Dabus
@LeoDabus Oh thanks, it fixed the error. If you post it as an answer I can mark it. Also would you mind explaining the difference?Maysam
I have never used the other method. Thats the one I usually use when I need.Leo Dabus
@LeoDabus, mind to post it as an answer?Maysam

1 Answers

1
votes

You can try unarchiveTopLevelObjectWithData to unarchive your AttributedString object data:

NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)

Your AttributedString implemented as a struct should look something like this:

struct AttributedString {
    let attributedString: NSAttributedString
    init(attributedString: NSAttributedString) { self.attributedString = attributedString }
    init(string str: String, attributes attrs: [NSAttributedString.Key: Any]? = nil) { attributedString = .init(string: str, attributes: attrs) }
}

Archiving / Encoding

extension NSAttributedString {
    func data() throws -> Data { try NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: false) }
}

extension AttributedString: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(attributedString.data())
    }
}

Unarchiving / Decoding

extension Data {
    func topLevelObject() throws -> Any? { try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(self) }
    func unarchive<T>() throws -> T? { try topLevelObject() as? T }
    func attributedString() throws -> NSAttributedString? { try unarchive() }
}

extension AttributedString: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        guard let attributedString = try container.decode(Data.self).attributedString() else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Corrupted Data")
        }
        self.attributedString = attributedString
    }
}