Reading JSON into array then writing it back to JSON Swift? - ios

New to IOS development and JSON stuff. I have a struct for Recipe which includes things like name, ingredients, instructions, etc. I have an array of Recipes. When my app is first run, I read data from a JSON file into the array of recipes so the app isn't empty at first. Throughout the app I append to the array of recipes. How would I go about writing the array back to the file everytime the array is changed? Here is some of the code and things I have tried.
Recipe Struct:
struct Recipe : Codable, Identifiable {
var id: String
var name: String
var ingredients: [String]
var instructions: [String]
var category: String
var imageName: String
}
Reading from JSON into recipe array:
import UIKit
import SwiftUI
var recipeData: [Recipe] = loadJson("recipeData.json")
func loadJson<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename,withExtension: nil)
else {
fatalError("\(filename) not found.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Could not load \(filename): \(error)")
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
fatalError("Unable to parse \(filename): \(error)")
}
}
My attempt to write back to a json file once array is changed(appended to):
func writeJson(){
var jsonArray = [String]()
if let documentDirectory = FileManager.default.urls(for: .documentDirectory,
in: .userDomainMask).first {
let pathWithFilename = documentDirectory.appendingPathComponent("test.json")
for recipe in recipeData{
do{
let encodedData = try JSONEncoder().encode(recipe)
let jsonString = String(data: encodedData, encoding: .utf8)
print(jsonString!)
jsonArray.append(jsonString!)
try jsonString!.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
}catch{
print(error)
}
}
}
}
This all builds successfully but nothing is written to my tests.json file. I am very new so any help would be appreciated. Thank you.

try jsonString!.write(to: pathWithFilename,
atomically: true,
encoding: .utf8)
This method erases the existing file and replaces it with the data in question. You do this in a loop, so you're going to overwrite the file many times, always with a single recipe. My expectation is that this would always leave just the last recipe in the file.
I believe what you meant to do is:
// Encode all the recipes, not one at a time.
let encodedData = try JSONEncoder().encode(recipeData)
// Write them. There's no reason to convert to a string
encodedData.write(to: pathWithFilename, options: [.atomic])
As a beginner, this is probably fine. A more professional approach would likely spread this data over multiple files, use a database, or Core Data. But for small projects with only a few data items, writing a single JSON file is fine.

Related

Write Dictionary to File in Swift

I'm trying to write a simple Dictionary to a .plist file in swift. I have written the code in an extension of the Dictionary class, for convenience. Here's what it looks like.
extension Dictionary {
func writeToPath (path: String) {
do {
// archive data
let data = try NSKeyedArchiver.archivedData(withRootObject: self,
requiringSecureCoding: true)
// write data
do {
let url = URL(string: path)
try data.write(to: url!)
}
catch {
print("Failed to write dictionary data to disk.")
}
}
catch {
print("Failed to archive dictionary.")
}
}
}
Every time I run this I see "Failed to write dictionary data to disk." The path is valid. I'm using "/var/mobile/Containers/Data/Application/(Numbers and Letters)/Documents/favDict.plist".
The dictionary is type Dictionary<Int, Int>, and contains only [0:0] (for simplicity/troubleshooting purposes).
Why doesn't this work? Do you have a better solution for writing the dictionary to disk?
URL(string is the wrong API. It requires that the string starts with a scheme like https://.
In the file system where paths starts with a slash you must use
let url = URL(fileURLWithPath: path)
as WithPath implies.
A swiftier way is PropertyListEncoder
extension Dictionary where Key: Encodable, Value: Encodable {
func writeToURL(_ url: URL) throws {
// archive data
let data = try PropertyListEncoder().encode(self)
try data.write(to: url)
}
}

JSON Parsing using multiple cores

iOS devices are getting better, have more cores but how can we get benefit out of it while we are parsing JSON?
Currently,
I am using JSONDecoder() for JSON Parsing. Is there way we can do it faster? Maybe using multiple threads parsing in parts etc.
Any hints/pointers will be appreciated.
import Foundation
let filePath = Bundle.main.path(forResource: "json", ofType: "json")
struct Vehicle: Codable {
private let color: String
private let tires: [Tire]
private let number: String
}
struct Tire: Codable {
private let company: String
private let isNew: Bool
}
func parseData(_ data: Data) {
let decoder = JSONDecoder()
try? decoder.decode([Vehicle].self, from: data)
}
func modifiedParsing(_ data: Data) {
}
let data = try String(contentsOfFile: filePath!).data(using: .utf8)
let date = Date()
let start = date.timeIntervalSince1970
//parseData(data!)
let end = Date().timeIntervalSince1970
print("Time Taken \(end-start)")
/*
Initial Times: [3.728722095489502, 3.5913820266723633, 3.5568389892578125, 3.534559965133667, 3.506725311279297]
After Changes
*/
I wanted to make JSON Parsing faster.
For anyone who is looking for a good solution please refer: https://github.com/bwhiteley/JSONShootout
Marshal is faster than codable.

how to read and write to the same file using swift with Xcode developing an iOS App

I am entering the Swift/Xcode-World coming from the Java-World and are struggling with File -path and -access. My App (iOS) needs to open a number of json-Files, encodes them to structs, and on every change which occurs - the user edits data - the same structs should be saved again as the same json files.
This instant structs/json-synchronization is essential to not loose data in case of a crashed App or iPad.
Everything works as it should - but the simulator saves the files at a different location as the input files, although the path looks to same (to me at least..). I know I should use observable classes instead of #state structs, but the model is rather complex and it would be a pain in the a. to convert the structs to classes. Anyway I guess this is not causing my file problem.
This is where I load the json`s:
func load_fuelHeader (_ filename: String) -> fuelheader{
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(fuelheader.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(fuelheader.self):\n\(error)")
}
}
This is the call to this func:
#State var FuelData=load_fuelHeader ("FuelHeader.json")
This is how I save the structs back to json`s:
func saveData(){
var jsonString:String?
let jsonEncoder = JSONEncoder()
do{
let jsonData = try jsonEncoder.encode(myFuelData)
jsonString=String(data: jsonData, encoding: String.Encoding.utf8)
} catch {
print (error)
}
guard let SaveFile = Bundle.main.url(forResource: "FuelHeader.json", withExtension: nil) else { fatalError("Couldn't find \("FuelHeader.json") in main bundle.") }
do {
try jsonString?.write(to: SaveFile, atomically: true, encoding: String.Encoding.utf8)
} catch {
print(error)
}
}
During debugging the loaded file is
file:///Users/.../Library/Developer/CoreSimulator/Devices/BBEDCD2E-05E0-4261-A08F-D214D101980F/data/Containers/Bundle/Application/8C92D2FB-344F-4516-AA22-32F54EE1386C/ofp.app/FuelHeader.json
while the saved file is the same, as it should be:
file:///Users/.../Library/Developer/CoreSimulator/Devices/BBEDCD2E-05E0-4261-A08F-D214D101980F/data/Containers/Bundle/Application/8C92D2FB-344F-4516-AA22-32F54EE1386C/ofp.app/FuelHeader.json
I checked the file, it is correct, it contains the changed data.The Problem is, the app does not load the above file, instead it loads the one within the xcode-project folder:
/Users/.../Documents/swift-projects/ofp/ofp/ressources/FuelHeader.json
for sure I missed some basics here, but I can`t find a solution yet.

Text file to array in swift

I'm trying to create a dictionary app on IOS. I have a text file (words_alpha.txt) in my Bundle, and I want to read all the words/lines, and place them into an arrray. String = ["word1", "word2", "word3"]. Here is my current code I got from bit.ly/39IC642
let path = Bundle.main.path(forResource: "words_alpha", ofType: "txt") // file path for file "words_alpha.txt"
let string = try String(contentsOfFile: path!, encoding: String.Encoding.utf8)
I am getting this error: Cannot use instance member 'path' within property initializer; property initializers run before 'self' is available
I am fairly new to using Swift and coding in general, please be detailed with your answers. Thank you!
If you are writing an iOS app, you can move such initialization into viewDidLoad():
var wordArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
//...
let url = Bundle.main.url(forResource: "words_alpha", withExtension: "txt")! // file URL for file "words_alpha.txt"
do {
let string = try String(contentsOf: url, encoding: .utf8)
wordArray = string.components(separatedBy: CharacterSet.newlines)
} catch {
print(error)
}
}
If your words_alpha.txt does contain multiple words per line, you may need some other way.

Unable to Write Array of Dict to Plist File

I am unable to write array of dicts into plist file. I can write Dictionary to plist file is ok.
My code is as follows; It is always not success
if var plistArray = NSMutableArray(contentsOfFile: path)
{
plistArray.add(dict)
let success = plistArray.write(toFile: path, atomically: true)
if success
{
print("success")
}
else
{
print("not success")
}
}
What might be wrong?
BR,
Erdem
First of all do not use NSMutableArray in Swift at all, use native Swift Array.
Second of all don't use the Foundation methods to read and write Property List in Swift, use PropertyListSerialization.
Finally Apple highly recommends to use the URL related API rather than String paths.
Assuming the array contains
[["name" : "foo", "id" : 1], ["name" : "bar", "id" : 2]]
use this code to append a new item
let url = URL(fileURLWithPath: path)
do {
let data = try Data(contentsOf: url)
var array = try PropertyListSerialization.propertyList(from: data, format: nil) as! [[String:Any]]
array.append(["name" : "baz", "id" : 3])
let writeData = try PropertyListSerialization.data(fromPropertyList: array, format: .xml, options:0)
try writeData.write(to: url)
} catch {
print(error)
}
Consider to use the Codable protocol to be able to save property list compliant classes and structs to disk.
I think you are missing the serialization part, you need to convert your plist file to data first and then write to file.
let pathForThePlistFile = "your plist path"
// Extract the content of the file as NSData
let data = FileManager.default.contents(atPath: pathForThePlistFile)!
// Convert the NSData to mutable array
do{
let array = try PropertyListSerialization.propertyList(from: data, options: PropertyListSerialization.MutabilityOptions.mutableContainersAndLeaves, format: nil) as! NSMutableArray
array.add("Some Data")
// Save to plist
array.write(toFile: pathForThePlistFile, atomically: true)
}catch{
print("An error occurred while writing to plist")
}

Resources