2

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 []
        }
    }
}
Optimalist
  • 313
  • 2
  • 11
  • Does it work??? The alternative is to implement Codable for `NSMutableAttributedString`, which might look nicer but shouldn’t change whether it works or not. – Fabian Aug 18 '18 at 21:22
  • @Purpose, I haven't figured out yet how I would even test it. I am still new to OOP, am not a programmer, and only code sporadically, for lack of time. But it also looks wrong. For example, the explicitly defined decoder and encoder seem disconnected from the JSONDecoder and -Encoder. 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. I'm even confused by the decoder init: Given its presence, how would I write an initializer for my properties? About your alternative, I wouldn't know how to do that. – Optimalist Aug 19 '18 at 05:42
  • The `NSCoding` part can work only if the class inherits from `NSObject` and adopts `NSCoding` – vadian Aug 19 '18 at 06:38
  • @vadian, then why does my code now seem to work (as tested in a playground)? NSKeyedArchiver and NSKeyedUnarchiver both inherit from NSCoder. Now, on top of everything else, I'm confused about the difference between NSCoder and NSCoding. Maybe the above code works because I'm not using NSCoding. – Optimalist Aug 19 '18 at 08:27
  • Maybe Playgrounds behave differently. In a regular project you will get compiler errors I guess. – vadian Aug 19 '18 at 08:41
  • @vadian, I just integrated (a more complex variant of) the above code into my app, and it ran without error in the simulator. It's not very satisfying using code that I don't really understand, but it's definitely satisfying that I have something that does what I need it to. – Optimalist Aug 19 '18 at 09:19

1 Answers1

1

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.

Fabian
  • 5,040
  • 2
  • 23
  • 35
  • Thank you for the very thorough explanation! But since my updated code now works, I don't think I'll be using your alternative. (Initially you suggested that implementing Codable for NSMutableAttributedString would "look nicer," but it seems to involve writing _a lot_ more code, no? Is it that it produces a more readable result?) In any case, I had already read the Raywenderlich Tutorial, but I was still unclear. I still think in procedural code terms, so I have a hard time understanding when the De-/Encoder and JSONDe-/JSONEncoder parts get called, and by what mechanism. – Optimalist Aug 19 '18 at 09:39
  • From your remarks, it seems that my use of custom implementations of Codable together with JSONEncoder/JSONDecoder is just how its done. Since both use an encoder and decoder, I wasn't sure if the JSON-type was supposed to substitute or complement the regular Codable type. (I'm probably misusing the jargon.) I guess it's the latter. And my use of NSKeyedUnarchiver/NSKeyedArchiver just transformed my NSMutableAttributedString into a form that Codable could handle. – Optimalist Aug 19 '18 at 09:54