Is there a way to find what exceptions are thrown by Foundation methods in Swift 3? In particular, I'm using the FileManager class to copy a file from the app bundle to the document directory. The copyItem method throws an exception if the destination file exists. How to catch that specific exception? The only way I've found is to parse the localizedDescription, which is ugly and likely not robust:
func copySampleReport() {
let fileManager = FileManager()
let reportName = "MyFile.txt"
if let documentUrl = try? fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false), let reportUrl = Bundle.main.url(forResource: URL(fileURLWithPath: reportName).deletingPathExtension().lastPathComponent, withExtension: URL(fileURLWithPath: reportName).pathExtension) {
debugPrint("Copying \(reportUrl) to \(documentUrl)")
do {
try fileManager.copyItem(at: reportUrl, to: documentUrl.appendingPathComponent(reportName))
debugPrint("File copied succesfully!")
}
catch {
if error.localizedDescription.range(of:"exists") != nil {
debugPrint("File exists at destination")
} else {
debugPrint(error.localizedDescription)
}
}
}
}
Related
I have a Swift console app where I'm trying to open a simple JSON file that I've included in a bundle for the console app. I'm able to load the bundle and get a path to the file and I've verified in terminal that the path exists. However when I attempt to get the contents I get an error
"Error Domain=NSCocoaErrorDomain Code=260 "The file “configuration.json” couldn’t be opened because there is no such file." UserInfo={NSFilePath=file:///Users/myName/Library/Developer/Xcode/DerivedData/JsonConfigurationReader-femnivebllduwxekfipxluxynhbj/Build/Products/Debug/Configuration.bundle/Contents/Resources/configuration.json, NSUnderlyingError=0x10630c080 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}"
I've pretty much followed the instructions of this SO post: Read a file in MacOS Command Tool Project
Here is the code I'm using:
import Foundation
// MARK: - Configuration
struct Configuration: Codable { //Representation of structure of JSON data
let developer: String
static func readJsonData(forName name: String) -> Data? { //Function to read JSON file and return data
do {
let currentDirectoryUrl = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
let bundleUrl = URL(fileURLWithPath: "Configuration.bundle", isDirectory: true, relativeTo: currentDirectoryUrl)
let bundle = Bundle(url: bundleUrl)
if let jsonFileURL = bundle?.url(forResource: "configuration", withExtension: "json") {
let contents = try String(contentsOfFile: jsonFileURL.absoluteString)
// let jsonData = json?.data(using: .utf8)
return nil //jsonData
}
} catch {
print(error)
}
return nil
}
static func parse(jsonData: Data) { //Parsing the JSON data
do {
let configuration = try JSONDecoder().decode(Configuration.self, from: jsonData)
print("developer: ",configuration.developer)
} catch {
print("decoded error")
}
}
}
Here is main.swift:
import Foundation
let developer = Configuration.readJsonData(forName: "configuration")
print("\(developer)")
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
}
}
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)
}
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.
I've been trying to figure out the best solution for data persistence for an app I'm working on and I decided a locally stored JSON file will be the best balance of simplicity and functionality.
What I need to save is an array of custom structs, and I can write it just fine using the code below, but I can't decode it back, I'm getting this error:
Cannot invoke 'decode' with an argument list of type '([Idea], from:
Data)'
Any idea what's causing this? Is the ideas array itself supposed to be Codable? Encoding it shouldn't have worked then right? Am I doing something else wrong?
Any suggestions would be appreciated.
var ideas = [Idea]()
--
struct Idea: Codable {
var title: String
var description: String?
var date: String
var completed: Bool
}
--
func writeIdeasToJSON(){
let pathDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
try? FileManager().createDirectory(at: pathDirectory, withIntermediateDirectories: true)
let filePath = pathDirectory.appendingPathComponent("data.json")
let json = try? JSONEncoder().encode(ideas)
do {
try json!.write(to: filePath)
} catch {
print("Failed to write JSON data: \(error.localizedDescription)")
}
}
--
func readIdeasFromJSON(){
do {
let path = Bundle.main.path(forResource: "data", ofType: "json")
let jsonData = try Data(contentsOf: URL(fileURLWithPath: path!))
do {
let readIdeas = try JSONDecoder().decode(ideas.self, from: jsonData)
print(readIdeas)
} catch let error{
print(error.localizedDescription)
}
} catch let error {
print(error.localizedDescription)
}
}
You have two issues. First is the compiler error from:
let readIdeas = try JSONDecoder().decode(ideas.self, from: jsonData)
That needs to be :
let readIdeas = try JSONDecoder().decode([Idea].self, from: jsonData)
With that fixed you get a runtime error because you wrote the file to the Documents folder but you attempt to read it from the app's resource bundle.
Update your loading code to use the same path used to save the file:
func readIdeasFromJSON(){
do {
let pathDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let filePath = pathDirectory.appendingPathComponent("data.json")
let jsonData = try Data(contentsOf: filePath)
let readIdeas = try JSONDecoder().decode([Idea].self, from: jsonData)
print(readIdeas)
} catch {
print(error)
}
}