I am writing a JSON file to documents directory, I would like to keep it in one file and read it later. The struct looks like this:
struct SymptomD:Codable
{
var symptom:String
var severity:String
var comment:String
var timestamp:String
}
Then I write to documents like so:
var completeData = SymptomD(symptom: "", severity: "", comment: "", timestamp: "")
func writeTrackedSymptomValues(symptom: String, comment: String, time: String, timestamp: String) {
completeData.symptom = symptom
completeData.severity = self.severity
completeData.comment = comment
completeData.timestamp = timestamp
createJSON()
}
var logFile: URL? {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let fileName = "symptom_data.json"
return documentsDirectory.appendingPathComponent(fileName)
}
func createJSON() {
guard let logFile = logFile else {
return
}
let jsonData = try! JSONEncoder().encode(completeData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(completeData) //This does not work, I am not sure how to add data without overwriting the previous file.
fileHandle.closeFile()
}
} else {
do {
try JSONEncoder().encode(completeData)
.write(to: logFile)
} catch {
print(error)
}
}
}
With this I can only add the data once, I am not sure how I should go about adding another 'row' basically to the JSON file, so that I can read these and decode them with my struct for use in a tableView
later. The JSON file made looks like this:
What is a way I can call the createJSON
function again, without overwriting the whole file, and how should I go about organising this so that when I read the JSON I can decode it simply and access the info.
Update:
Using this I am able to add more lines to the JSON,
let jsonData = try! JSONEncoder().encode(completeData)
let jsonString = String(data: jsonData, encoding: .utf8)!
print(jsonString)
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(jsonData)
fileHandle.closeFile()
}
Giving me this:
{"timestamp":"1592341465","comment":"","severity":"Mild","symptom":"Anxiety"}{"timestamp":"1592342433","comment":"","severity":"Moderate","symptom":"Anxiety"}{"timestamp":"1592342458","comment":"","severity":"Mild","symptom":"Anxiety"}{"timestamp":"1592343853","comment":"","severity":"Mild","symptom":"Anxiety"}{"timestamp":"1592329440","comment":"","severity":"Mild","symptom":"Fatigue"}{"timestamp":"1592344328","comment":"","severity":"Mild","symptom":"Mood Swings"}{"timestamp":"1592257920","comment":"test","severity":"Mild","symptom":"Anxiety"}
But when trying to parse this, it crashes with an error:
Code=3840 "Garbage at end."
What am I doing wrong?
The issue looks pretty clear to me. You are appending another dictionary to an existing dictionary but you should have created an array of dictionaries to be able to append a dictionary to it.
struct SymptomD: Codable {
var symptom, severity, comment, timestamp: String
init(symptom: String = "", severity: String = "", comment: String = "", timestamp: String = "") {
self.symptom = symptom
self.severity = severity
self.comment = comment
self.timestamp = timestamp
}
}
If you would like to manually append the text to your json string you will need to seek to the position before the end of your file, add a comma before the next json object and a closed bracket after it:
extension SymptomD {
func write(to url: URL) throws {
if FileManager.default.fileExists(atPath: url.path) {
let fileHandle = try FileHandle(forWritingTo: url)
try fileHandle.seek(toOffset: fileHandle.seekToEndOfFile()-1)
let data = try JSONEncoder().encode(self)
fileHandle.write(Data(",".utf8) + data + Data("]".utf8))
fileHandle.closeFile()
} else {
try JSONEncoder().encode([self]).write(to: url)
}
}
}
Playground testing:
var logFile: URL? {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("symptom_data.json")
}
var symptomD = SymptomD()
symptomD.symptom = "Anxiety"
symptomD.severity = "Mild"
symptomD.timestamp = .init(Date().timeIntervalSince1970)
do {
if let logFile = logFile {
try symptomD.write(to: logFile)
}
} catch {
print(error)
}
var symptomD2 = SymptomD()
symptomD2.symptom = "Depression"
symptomD2.severity = "Moderate"
symptomD2.timestamp = .init(Date().timeIntervalSince1970)
do {
if let logFile = logFile {
try symptomD2.write(to: logFile)
}
} catch {
print(error)
}
do {
if let logFile = logFile {
let symptoms = try JSONDecoder().decode([SymptomD].self, from: .init(contentsOf: logFile))
print(symptoms)
}
} catch {
print(error)
}
This will print:
[__lldb_expr_532.SymptomD(symptom: "Anxiety", severity: "Mild", comment: "", timestamp: "1592356106.9662929"), __lldb_expr_532.SymptomD(symptom: "Depression", severity: "Moderate", comment: "", timestamp: "1592356106.978864")]
edit/update:
If you need to update a single "row" of your JSON, you will need to make your struc conform to equatable, read your collection and find its index:
extension SymptomD: Equatable {
static func ==(lhs: SymptomD, rhs: SymptomD) {
(lhs.symptom, lhs.severity, lhs.comment ,lhs.timestamp) ==
(rhs.symptom, rhs.severity, rhs.comment ,rhs.timestamp)
}
@discardableResult
mutating func updateAndWrite(symptom: String? = nil, severity: String? = nil, comment: String? = nil, timestamp: String? = nil, at url: URL) throws -> [SymptomD]? {
var symptoms = try JSONDecoder().decode([SymptomD].self, from: .init(contentsOf: url))
if let index = symptoms.firstIndex(of: self) {
self.symptom = symptom ?? self.symptom
self.severity = severity ?? self.severity
self.comment = comment ?? self.comment
self.timestamp = timestamp ?? self.timestamp
symptoms[index] = self
try JSONEncoder().encode(symptoms).write(to: url, options: .atomic)
return symptoms
}
return nil
}
}