Search code examples
jsonswiftxcodecodablensdocumentdirectory

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


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?


Solution

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