0

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)")
    }
}
Arnav Motwani
  • 707
  • 7
  • 26
  • What is `Book`? Show us `Book`. – Rob Oct 18 '20 at 06:54
  • I would separate the encode and write into two separate `try`s. One try encodes, the other writes the encoded result. Does your write function throw any errors? Have you checked the documents directory to see if the file actually exists? (Print the file url and open it in finder) Have you considered that you may not be loading the saved json correctly and you end up always loading the default? – Andrew Oct 18 '20 at 07:01
  • To me it looks like the problem lies with what you have in your array of Book so the issue is not with the posted code but elsewhere in your app. – Joakim Danielson Oct 18 '20 at 07:32
  • @Andrew I am confused, I got this code on hacking with swift and since it worked to some degree I thought it was fine.... but this is an iOS app. There were no errors being thrown. by the function – Arnav Motwani Oct 18 '20 at 07:41
  • @Rob I edited it onto the question – Arnav Motwani Oct 18 '20 at 07:42
  • @JoakimDanielson Between your comment and Andrews comment, I realised that my code may not be encoding the array at all and the updated list is just a result of the appended array that is bonded to my content view. Maybe my url is wrong considering its an iOS app? – Arnav Motwani Oct 18 '20 at 07:47
  • I have tested your code in a playground and I can add a new Book to the array and both my new book and the ones in your json gets saved to file. As I said, I think the issue is somewhere else in your code. Use a debugger or `print()` statements to try to figure out what happens. For starters print `bookData` in your `writeJSON` function. – Joakim Danielson Oct 18 '20 at 08:46
  • @JoakimDanielson I printed book data, the fileURL and the encoded json and all three look okay (expect the fields are in a different order in the encoded json). I printed the book I am appending onto the array and it printed fine. Its just the saving doesn't seem to be happening. I am not sure where the mistake is. – Arnav Motwani Oct 18 '20 at 10:24
  • @JoakimDanielson I tried something else, I added ```let data: [Book] = load("list.json") print(data)``` to the function after the write and it prints the original json not the new json. There is a mistake with my write function. – Arnav Motwani Oct 18 '20 at 10:30
  • what the code in load("list.json") – Blazej SLEBODA Oct 18 '20 at 11:07
  • @Adobels I edited the code onto the question – Arnav Motwani Oct 18 '20 at 12:06
  • @ArnavMotwani The issue there is that you are saving your json in a different location. The bundle directory is readonly. You need to make a copy of the json and save it at any directory in your app that you have read-write access (documents, library, application support folder, etc...) – Leo Dabus Oct 18 '20 at 23:27

1 Answers1

1

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)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Yes that's what I thought after reading joakim's reply. How ever there is an error I get when I use ``` @State var bookData: [Book] = load("list.json") ``` in my content view, I get the error (Call can throw, but errors cannot be thrown out of a property initializer) and when I call writeJSON using a button I get this error (Call can throw, but it is not marked with 'try' and the error is not handled) – Arnav Motwani Oct 19 '20 at 05:48
  • You can get rid of the throw in the methods and use `try?` instead of `try` – Leo Dabus Oct 19 '20 at 05:57
  • if you are sure that the json is correct and the file exists you can also use `try!` and return non optional type. – Leo Dabus Oct 19 '20 at 06:00
  • 1
    Yes that worked, I force unwrapped the return try in load but I used try optional – Arnav Motwani Oct 19 '20 at 06:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/223264/discussion-between-arnav-motwani-and-leo-dabus). – Arnav Motwani Oct 19 '20 at 07:41