Search code examples
jsonswiftswiftuicodableencodable

JSONencoder not saving new encoded json array (iOS)


I have a json file that looks like this (in a file called list.json)

[
  {
    "id": "C8B046E9-70F5-40D4-B19A-40B3E0E0877B",
    "name": "Dune",
    "author": "Frank Herbert",
    "page": "77",
    "total": "420",
    "image": "image1.jpg"
  },
  {
    "id": "2E27CA7C-ED1A-48C2-9B01-A122038EB67A",
    "name": "Ready Player One",
    "author": "Ernest Cline",
    "page": "234",
    "total": "420",
    "image": "image1.jpg"
  }
]

This a default file that comes with my app (These are examples that can be deleted). My content view has a member variable that uses a decode function I wrote to get the json array and display it in a list. I have a view to add another book to the json file. The view appends another struct to the array and then encodes the new appended array to list.json with this function

func writeJSON(_ bookData: [Book]) {
    do {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
            .appendingPathComponent("list.json")

        let encoder = JSONEncoder()
        try encoder.encode(bookData).write(to: fileURL)
    } catch {
        print(error.localizedDescription)
    }
}

This function is called in the NewBook view when a button is pressed. bookData is the decoded array in my content view which I used a Binding to in my NewBook view.

The code works if you add the book and go back to the contentview (the list now contains the appended struct) but if you close the app and open it again, the list uses the default json file. I think there is a mistake in my writeJSON function.

Also note that I tried changing the create parameter to false in the URL but that didn't help.

edit: I am adding the Book struct as requested

struct Book: Hashable, Codable, Identifiable {
    var id: UUID
    var name: String
    var author: String
    var page: String
    var total: String
    var image: String
}

edit 2: This is for an iOS app

edit 3: my load data function

func load<T: Decodable>(_ filename: String) -> T {
    let data: Data
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

Solution

  • You are probably not overriding the existing file on disk. Try options: .atomic while writing the data to disk.

    func writeJSON(_ bookData: [Book]) {
        do {
            let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("list.json")
            try JSONEncoder().encode(bookData).write(to: fileURL, options: .atomic)
        } catch {
            print(error)
        }
    }
    

    edit/update:

    The issue here is that you are not saving the file where you think it would. The Bundle directory is read-only and has no relation with your App documents directory.

    func load<T: Decodable>(_ filename: String) -> T? {
        // no problem to force unwrap here you actually do want it to crash if the file it is not inside your bundle
        let readURL = Bundle.main.url(forResource: filename, withExtension: "json")!
        let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let jsonURL = documentDirectory
            .appendingPathComponent(filename)
            .appendingPathExtension("json")
        // check if the file has been already copied from the Bundle to the documents directory
        if !FileManager.default.fileExists(atPath: jsonURL.path) {
            // if not copy it to the documents (not it is not a read-only anymore)
            try? FileManager.default.copyItem(at: readURL, to: jsonURL)
        }
        // read your json from the documents directory to make sure you get the latest version
        return try? JSONDecoder().decode(T.self, from: Data(contentsOf: jsonURL))
    }
    func writeJSON(_ bookData: [Book]) {
        let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
        let jsonURL = documentDirectory
            .appendingPathComponent("list")
            .appendingPathExtension("json")
        // write your json at the documents directory and use atomic option to override any existing file
        try? JSONEncoder().encode(bookData).write(to: jsonURL, options: .atomic)
    }