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)
}
}
Related
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.
I created a stop button that can collect data, which will be saved to the defined path after clicking the stop button. However, if I want to continue collecting after clicking the stop button, the data will be added to the original text file. (This makes senses as I only know how to define one path)
My question is: Would it be possible to ask the user and input a new file name and save as a new text file after each stop so that the data is not added to the original file?
Below is what I have for one defined path and stacking up the data:
#IBAction func stopbuttonTapped(_ btn: UIButton) {
do {
let username:String = user_name.text!
fpsTimer.invalidate() //turn off the timer
let capdata = captureData.map{$0.verticesFormatted}.joined(separator:"") //convert capture data to string
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("testing.txt") //name the file
try capdata.appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
}
And the extension I use for string and data:
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self).appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
func trim() -> String
{
return self.trimmingCharacters(in: CharacterSet.whitespaces)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
Do I need to set a default file name (maybe texting.txt) and then popped up a user input for saving the text file? (That's where I am not too sure how to integrate to what I already have). I thank you so much for your time and suggestions in advance.
You could generate unique names.
For example:
let url = dir.appendingPathComponent("testing-\(Date.timeIntervalSinceReferenceDate).txt")
or
let url = dir.appendingPathComponent("testing-\(UUID().uuidString).txt")
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.
so this might be a trivial question, but I can't get it to work.
I want to save a pdf file to CoreData after I dropped it onto a view (using IOS' Drag&Drop feature)
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
session.loadObjects(ofClass: ComicBookPDFDocument.self) { (pdfItems) in
let items = pdfItems as! [ComicBookPDFDocument]
// "Cannot assign value of type 'ComicBookPDFDocument' to type 'NSData?'"
self.file.data = items[0]
}
}
ComicBookPDFDocument just subclasses PDFDocument to make it conforming to NSItemProviderReading:
final class ComicBookPDFDocument: PDFDocument, NSItemProviderReading {
public static var readableTypeIdentifiersForItemProvider: [String] {
return [kUTTypePDF as String]
}
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> ComicBookPDFDocument {
return ComicBookPDFDocument(data: data)!
}
}
However, I get this compiler error from XCode:
Cannot assign value of type 'ComicBookPDFDocument' to type 'NSData?'
How can I save the pdf data from a PDFDocument? I couldn't find anything on the internet or the documentation.
Thanks for any help
Okay, I don't know how I missed that, but here it is:
items[0].dataRepresentation()
You do one thing,
Try to save PDF into the Document Directory and save its path in the Core-Data.
Here is the code to save to Document directory and fetch from document direcory
class PDCache: NSObject {
static let sharedInstance = PDCache()
func saveData(obj: Data, fileName: String){
let filename = getDocumentsDirectory().appendingPathComponent("\(fileName).pdf")
do{
try obj.write(to: filename, options: .atomic)
} catch{
print(error.localizedDescription)
}
}
func getData(fileName: String) -> URL?{
let fileManager = FileManager.default
let filename = getDocumentsDirectory().appendingPathComponent("\(fileName).pdf")
if fileManager.fileExists(atPath: filename.path){
return URL(fileURLWithPath: filename.path)
}
return nil
}
private func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
}
To save the data use this
let data = Data()
self.saveData(obj: data, fileName: "myPdfFile")
and to get the file url use this
let pdfUrl = self.getData(fileName: "myPdfFile")
Try this and let me know if it works for you.
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")
}