Optimalist
Optimalist

Reputation: 323

NSCoding AND Codable Properties <=> JSON Format <=> (Read/Write) File

I need to read/write properties that are Codable (e.g., Date) and NSCoding (e.g., NSMutableAttributedString) from/to a JSON-formatted file. After looking into how to read from and write to files using Codable, how to do so in the JSON format, and how to combine NSCoding with Codable when some properties don't conform to Codable (but do conform to NSCoding), I kludged together the following code and confused myself in the process.

I finally figured out how to test this, and made changes accordingly. But I'd still like to know how the three decoder/encoder types (NSCoding, Codable, and JSON) interact or substitute for one another.

import Foundation

class Content: Codable {

    // Content
    var attrStr = NSMutableAttributedString(string: "")
    var date: Date?

    // Initializer for content
    init(attrStr: NSMutableAttributedString, date: Date) {
        self.attrStr = attrStr
        self.date = date
}

    // Need to explicitly define because NSMutableAttributedString isn't codable
    enum CodingKeys: String, CodingKey {

        case attrStr
        case date
    }

    // Need to explicitly define the decoder. . . .
    required init(from decoder: Decoder) throws {

        let container = try decoder.container(keyedBy: CodingKeys.self)

        date = try container.decode(Date.self, forKey: .date)

        let attrStrData = try container.decode(Data.self, forKey: .attrStr)
        attrStr = NSKeyedUnarchiver.unarchiveObject(with: attrStrData) as? NSMutableAttributedString ?? NSMutableAttributedString(string: "Error!")
    }

    // Need to explicitly define the encoder. . . .
    func encode(to encoder: Encoder) throws {

        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(date, forKey: .date)

        let attrStrData = NSKeyedArchiver.archivedData(withRootObject: attrStr)
        try container.encode(attrStrData, forKey: .attrStr)
    }

    static func getFileURL() -> URL {

        // Get the directory for the file
        let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        // Get the full path and filename
        return docsDir.appendingPathComponent("contentArray").appendingPathExtension("cntnt")
    }

    static func saveToFile(content: [Content]) {

        // Get the file's URL
        let fileURL = getFileURL()

        do {
            // Encode the data
            let data = try JSONEncoder().encode(content)
            // Write to a/the file
            try data.write(to: fileURL)

        } catch {
            print("Could not encode or save to the file!")
        }
    }

    static func loadFromFile() -> [Content] {

        // Get the file's URL
        let fileURL = getFileURL()

        do {
            // Read from the file
            let data = try Data(contentsOf: fileURL)
            // Decode the data
            return try JSONDecoder().decode([Content].self, from: data)

        } catch {
            print("Could not decode or read from the file!")
            return []
        }
    }
}

Upvotes: 2

Views: 1771

Answers (1)

Fabian
Fabian

Reputation: 5348

About your alternative, I wouldn't know how to do that.

I gave implementing Codable for NSMutableAttributedString a try. I Had to embed instead of subclassing it since it is a class-cluster. Source

class MutableAttributedStringContainer: Codable {
    let attributedString: NSMutableAttributedString

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

    public required init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)

        let archiver = try NSKeyedUnarchiver(forReadingFrom: data)
        attributedString = NSMutableAttributedString(coder: archiver)!
    }

    public func encode(to encoder: Encoder) throws {
        let archiver = NSKeyedArchiver(requiringSecureCoding: true)
        attributedString.encode(with: archiver)

        var container = encoder.singleValueContainer()
        try container.encode(archiver.encodedData)
    }
}

Here is an example how to use it.

func testing() {
    let attributedString = NSMutableAttributedString(string: "Hello world!")
    let attributedStringContainer = MutableAttributedStringContainer(attributedString: attributedString)

    // Necessary because encoding into a singleValueContainer() creates a
    // JSON fragment instead of a JSON dictionary that `JSONEncoder` wants
    // create.
    struct Welcome: Codable {
        var attributedString: MutableAttributedStringContainer
    }
    let welcome = Welcome(attributedString: attributedStringContainer)

    let encoder = JSONEncoder()
    encoder.outputFormatting = .prettyPrinted
    let data = try! encoder.encode(welcome)
    print(String(bytes: data, encoding: .utf8) as Any)

    let decoder = JSONDecoder()
    let welcome2 = try! decoder.decode(Welcome.self, from: data)
    print("decoded to string:", welcome2.attributedString.attributedString.string)
}

But it also looks wrong. For example, the explicitly defined decoder and encoder seem disconnected from the JSONDecoder and -Encoder.

Codable structures build on each other. If all the underlying structures implement Codable the compiler can create the encoding and decoding functions by itself. If not, the developer has to encode them and put them on a CodingKey, the same for decoding.

One could for example convert them to data in any way, then encode them as Data to a CodingKey. Maybe read a Raywenderlich Tutorial on Codable to understand it better.

There should be a discernible processing stream, but I can't see how the three kinds of decoders/encoders interact or substitute for one another.

There are decoders/encoders and methods that support the specific encoder/decoder-pair.

NSCoding works together with NSKeyedUnarchiver/NSKeyedArchiver and returns NSData which is just data though so not in a human-readable form.

Codable works together with any encoder/decoder pair which supports Codable, more specifically in our case JSONEncoder/JSONDecoder, which returns Data which is in the human-readable format JSON and can be printed since the data here is encoded in .utf8.

Upvotes: 1

Related Questions