JSONencoder not saving new encoded json array (iOS) - json

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

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

Related

Can't Decode JSON successfully from file (Swift)

O.
I have a JSON file in my XCode project. Lets use this as an example:
{ "shifts": [
{"name": "Mary",
"role": "Waiter"},
{"name": "Larry",
"role": "Cook"}
]
}
In my JSONManager.swift, I have something along the lines of this:
struct Shift: Codable {
let role, name: String
static let allShifts: [Shift] = Bundle.main.decode(file: "shifts.json")
}
extension Bundle {
func decode<T: Decodable>(file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Could not find \(file) in the project")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Could not load \(file) in the project")
}
let decoder = JSONDecoder()
guard let loadedData = try? decoder.decode(T.self, from: data) else {
fatalError("Could not decode \(file) in the project")
}
return loadedData
}
}
I keep hitting the fatalError("Could not decode \(file) in the project") and I'm wondering if it's because the JSON is not formatted correctly or why it can't decode the JSON from the JSON file.
Take a look at you JSON structure for a momement.
You have a element called shifts, which is any array of other elements, but your code seems to trying to load a structure which only contains the child element.
Instead, you need a outer struct which contains the child struct, for example...
struct Shift: Codable {
let role, name: String
}
struct Shifts: Codable {
let shifts: [Shift]
}
Then you'd use the parent struct as the initial source type when decoding the content...
extension Bundle {
func decode<T: Decodable>(file: String) -> T {
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Could not find \(file) in the project")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("Could not load \(file) in the project")
}
let decoder = JSONDecoder()
guard let loadedData = try? decoder.decode(T.self, from: data) else {
fatalError("Could not decode \(file) in the project")
}
return loadedData
}
}
let shifts: Shifts = Bundle.main.decode(file: "shifts.json")
I tested this in playgrounds and it worked fine.
There might be away to decode the structure without needing the "parent" struct, but this works for me.
I fixed my own mistake. I don't need the
{ "shifts": }
in my JSON

Issue decoding Base64 JSON data

Any pointers would be much appreciated. No build or run errors with the code below, but 239 "Fail" prints in the Console. Also, once I get it to work properly, this code should be in a separate function or extension and not part of the View, and for reusability. I am expecting to see a country's flag next to its name in a Picker listing. The 'country.name' displays and 'selectedCountry' can be selected for subsequent Core Data storage, but sadly no flag displays. The relevant code and a sample "Country" from the JSON file below:
In the Properties section:
let countryImage: [Country] = Bundle.main.decode("Countries.json")
#State private var selectedContact: [ContactEntity] = []
let flag = "flag"
In the View:
VStack {
Picker(selection: $selectedCountry,
label: Text("Host Country:")) {
ForEach(countryImage) { country in
HStack {
Text(country.name).tag(country.name)
// TODO - Find Flag
if case country.flag = Data(base64Encoded: flag), let uiImage = UIImage(data: country.flag) {
Image(uiImage: uiImage).tag(country.flag)
} else {
let _ = print("Fail")
}
//Image(flag)
}
}
}
.padding(.horizontal, 0)
.padding()
.pickerStyle(MenuPickerStyle())
}//: INNER PICKER VSTACK
The Bundle references an extension:
extension Bundle {
func decode<T: Codable>(_ file: String) -> T { // "_" obviates the need to call the name of the parameter
// 1. Locate the JSON file
guard let url = self.url(forResource: file, withExtension: nil) else {
fatalError("Failed to locate \(file) in bundle.")
}
// 2. Create a property for the data
guard let data = try? Data(contentsOf: url) else {
fatalError("Failed to load \(file) from bundle.")
}
// 3. Create a decoder
let decoder = JSONDecoder()
// 4. Create a property for the decoded data
guard let loaded = try? decoder.decode(T.self, from: data) else {
fatalError("Failed to decode \(file) from bundle.")
}
// 5. Return the ready-to-use data
return loaded
}
}
Sample JSON item:
{
"id": 202,
"name": "Sudan",
"isoAlpha2": "SD",
"isoAlpha3": "SDN",
"isoNumeric": 736,
"currency": {
"code": "SDD",
"name": "Dinar",
"symbol": false
},
"flag": "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAUCAYAAACaq43EAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozNjc2Q0MxMjE3ODgxMUUyQTcxNDlDNEFCRkNENzc2NiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozNjc2Q0MxMzE3ODgxMUUyQTcxNDlDNEFCRkNENzc2NiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkY1RTdDNTBCMTc4NzExRTJBNzE0OUM0QUJGQ0Q3NzY2IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkY1RTdDNTBDMTc4NzExRTJBNzE0OUM0QUJGQ0Q3NzY2Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+dsEThQAAAfVJREFUeNq8lT9oE1Ecxz/v3ZmkMae2xdbuiqRiqaugKDo5WFGQOiiimy5aR6dOOrhYHRx0qQoVaQOCbgpO/oHatTrVhCYNtIlJbWJJ7p6/C7aiVhfv5Qf3jrt33Of9vt/f7z118sTe+TvPE15XQzHbGaABZbAeihu7zc6sw71MB0cWXGY9n28uOJbhDgM910q9QXx8oEFnVXMsu6mVcSVuF64lZ1jUrYcrp2sMD9XwBbinpPFlzihb4LVxRQhlzdP9qxy6sMKbHT6DZYdYk9YCovd4JF2Vu7f+JpS3O4BVuPUiydXpBPMUKLLc+tweOAzhkpIVxJsMv1bm/rZTfrK7y2QrBWUi8t39qwGh9HWHiX1l9bF/znl8/rpJk1LReryxFiK7I0MPMy8nVf+lXfrhh2fqx8x/X/rf6xJdfR/60tAMOHdziMNjZyguL1rM+I+OF1caUKpXpd2MJY9/BxY/QSzJ3ZFxLh84a7G41toq3MJ0haMHj5tHFx+Y3giLa2NwIMDNMpWSje1V0WzJav/2zKjJfVlQgQksgUPo9oT4KYBMHvO+rKaYcKPeuX7+MCwYR2qtrwPmvsJUDgp1a4eE+4u0W2PwVlplMmv9PHbXpZU+5clnmF6iHeGKtB65mkgrWebrtCtc3i3lyeQ8jKGd8V2AAQDj9qv7QrTRKwAAAABJRU5ErkJggg=="
},
It's easier than expected. Decodable can decode a base64-encoded String directly as Data
In Country declare flag
let flag: Data?
and a computed property
var flagImage : UIImage? {
guard let flagData = flag else { return nil }
return UIImage(data: flagData)
}
And display the image
if let flagImage = country.flagImage {
Image(uiImage: flagImage).tag(country.flag)
} else {
let _ = print("Fail")
}

Saving changes from a loaded json File to Core Data in SwiftUI

i want to load my data with a json file. So far so good. But now I am struggling. What if the user made some changes to the Oil Object and want to save them? My idea was, that i save the changed oils object to CoreData. But what is this possible? Because every time the user launches the app, the untouched json file gets loaded and the user will not see his changed objects. How can i handle that? Or is my thinking wrong?
struct Oil: Codable, Hashable, Identifiable {
var id: Int
let image: String
let color: String
let title: String
let subtitle: String
let description: String
var localizedTitle: LocalizedStringKey {
return LocalizedStringKey(title)
}
var localizedDescription: LocalizedStringKey {
return LocalizedStringKey(description)
}
var isFavorite: Bool
static let exampleOil = Oil(id: 10001, image: "",color: "lavenderColor" ,title: "lavender", subtitle: "Lavandula angustifolia", description: "", isFavorite: false)
}
final class Oils: ObservableObject {
var oils: [Oil] = load("oilDatabase.json")
}
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)")
}
}
Do you really need Core Data? If you are just storing a single json file you could save yourself a lot of complexity and just write the updated version of the file to the documents directory.
All you would have to do is to check that the file exists in the documents directory, if it does load it from there otherwise load it from your bundle.
This gets the URL for the documents directory
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
Then we just need to update your load to check that the file exists in the documents directory before loading it.
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
// Check that the fileExists in the documents directory
let filemanager = FileManager.default
let localPath = getDocumentsDirectory().appendingPathComponent(filename)
if filemanager.fileExists(atPath: localPath.path) {
do {
data = try Data(contentsOf: localPath)
} catch {
fatalError("Couldn't load \(filename) from documents directory:\n\(error)")
}
} else {
// If the file doesn't exist in the documents directory load it from the bundle
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)")
}
}
You will also need to save any changes that you make to the json. You can do that with the following function.
func save<T: Encodable>(_ filename: String, item: T) {
let encoder = JSONEncoder()
do {
let url = getDocumentsDirectory().appendingPathComponent(filename)
let encoded = try encoder.encode(item)
let jsonString = String(data: encoded, encoding: .utf8)
try jsonString?.write(to: url, atomically: true, encoding: .utf8)
} catch {
// handle your error
}
}

Cannot find 'LocationsData' in scope

My project contains the JSON file 'LocationsData.json'
An error is displayed 'Cannot find 'LocationsData' in scope'
(If needed - the following code is contained within my 'Data.swift' file, which is what the var 'locationsDataTypes' in the 'SearchRedirect.swift' is referencing)
import UIKit
import SwiftUI
import CoreLocation
let locationsDataTypes: [LocationsDataTypes] = load("LocationsData.json")
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)")
}
}
final class ImageStore {
typealias _ImageDictionary = [String: CGImage]
fileprivate var images: _ImageDictionary = [:]
fileprivate static var scale = 2
static var shared = ImageStore()
func image(name: String) -> Image {
let index = _guaranteeImage(name: name)
return Image(images.values[index], scale: CGFloat(ImageStore.scale), label: Text(name))
}
static func loadImage(name: String) -> CGImage {
guard
let url = Bundle.main.url(forResource: name, withExtension: "jpg"),
let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),
let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
else {
fatalError("Couldn't load image \(name).jpg from main bundle.")
}
return image
}
fileprivate func _guaranteeImage(name: String) -> _ImageDictionary.Index {
if let index = images.index(forKey: name) { return index }
images[name] = ImageStore.loadImage(name: name)
return images.index(forKey: name)!
}
}
As pawello2222 hinted, locationsData isn't declared anywhere.
I suspect that you have your List view builder function written incorrectly.
Instead of this...
List(LocationsData, id: \.id) { locationsDataTypes in
Try the following...
List(locationsDataTypes, id: \.id) { locationsData in
...
Image(locationsData.imageName)
.resizable
...
}
Where locationsDataTypes is the collection, in your case the array of LocationsDataTypes and locationsData is the single instance of each iteration through that collection.
Also, when writing a question in stack overflow, best practice is to include the relevant code within a code block in your question, not accessed via a hyperlink. There are many reasons for this, here are a couple...
it is easier for the SO community to copy and paste your code into their solution as a part of their response, saving us all a lot of time;
links break, leaving your question without context for anyone who might have the same problem in the future.

How to save JSON data using SwiftyJSON

I am trying to save data localy using the JSON library. Here is my code at the moment:
var jsonFile = "json.json"
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonFile = "file.json"
let jsonArray:JSON = [
"array": [12.34, 56.78],
"users": [
[
"id": 987654,
"info": [
"name": "jack",
"email": "jack#gmail.com"
],
"feeds": [98833, 23443, 213239, 23232]
],
[
"id": 654321,
"info": [
"name": "jeffgukang",
"email": "jeffgukang#gmail.com"
],
"feeds": [12345, 56789, 12423, 12412]
]
]
]
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
{
let path = dir.appendingPathComponent(jsonFile)
if(!FileManager.default.fileExists(atPath: path.path))
{
FileManager.default.createFile(atPath: path.path, contents: Data(), attributes: nil)
print("Created file")
do {
try write(json: jsonArray, to: path)
} catch let error{
print("Error: \(error.localizedDescription)")
}
try! write(json: jsonArray, to: path)
} else {
print("File existed")
do {
let myJSON = try readJSON(myURL: path)
if let _ = myJSON
{
print("Got data")
} else {
print("No Data")
}
} catch let errorOne {
print("Error2: \(errorOne.localizedDescription)")
}
}
}
}
}
let url = Bundle.main.url(forResource: "json", withExtension: "json")
func readJSON(myURL:URL) throws -> JSON? {
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
{
let path = dir.appendingPathComponent(jsonFile)
if(!FileManager.default.fileExists(atPath: path.path))
{
let data = try Data(contentsOf: path, options: .alwaysMapped)
return JSON(data: data)
}
}
return nil
}
func write(json: JSON, to url: URL) throws {
let data = try json.rawData()
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
{
let path = dir.appendingPathComponent(jsonFile)
if(!FileManager.default.fileExists(atPath: path.path))
{
try data.write(to: path)
}
}
}
When you run the program for the first time, it creates the file and 'writes the jsonArray'. When you run it again, it 'loads' the JSON data.
The problem is, the value returned of readJSON(myURL: path) is equal to nil.
How would I read and write JSON data using SwiftyJSON on ios 10.2?
Update
I have executed my application in the iPhone 6s simulator and checked the saved file json.json. Here is the saved file:
{"array":[12.34,56.78],"users":[{"id":987654,"info":{"name":"jack","email":"jack#gmail.com"},"feeds":[98833,23443,213239,23232]},{"id":654321,"info":{"name":"jeffgukang","email":"jeffgukang#gmail.com"},"feeds":[12345,56789,12423,12412]}]}
I have just relised what the problem was.
In the readJSON() function:
func readJSON(myURL:URL) throws -> JSON? {
if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
{
let path = dir.appendingPathComponent(jsonFile)
if(!FileManager.default.fileExists(atPath: path.path))
{
let data = try Data(contentsOf: path, options: .alwaysMapped)
return JSON(data: data)
}
}
return nil
}
the line if(!FileManager.default.fileExists(atPath: path.path)) should have been if(FileManager.default.fileExists(atPath: path.path)).
Before, I was only loading the data if the file did not exist.