How do I correctly write this data to a JSON file without overwriting the file?

1.1k Views Asked by At

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:

enter image description here

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?

1

There are 1 best solutions below

13
Leo Dabus On BEST ANSWER

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
    }
}