Reputation: 323
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
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