I'm retrieving a plist file, updating it, and writing it to disk.
1) Retrieval
func pListURL() -> URL? {
guard let result = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true).appendingPathComponent("somePlist.plist") else { return nil }
return result
}
2) Update
var data: [String: Int] = [:]
if let url = pListURL() {
do {
let dataContent = try Data(contentsOf: url)
if let dict = try PropertyListSerialization.propertyList(from: dataContent, format: nil) as? [String: Int] {
data = dict
}
} catch {
print(error)
}
}
// update code
3) Write
if let path = pListURL() {
do {
let plistData = try PropertyListSerialization.data(fromPropertyList: data, format: .xml, options: 0)
try plistData.write(to: path)
} catch {
print(error)
}
}
The odd thing is I get an error that says:
The file “somePlist.plist” couldn’t be opened because there is no such file.
even though when I check the plist, it's actually properly created and updated as it should be. As far as I know, the create parameter of FileManager.default.url(for:in:appropriateFor:create: ) ensures that it "creates the directory if it does not already exist", which means somePlist.plist is created if plist doesn't exist already.
As far as I know, the create parameter of FileManager.default.url(for:in:appropriateFor:create: ) ensures that it "creates the directory if it does not already exist", which means somePlist.plist is created if plist doesn't exist already.
No, it means the directory is created but the file is not created.
In the update part ignore the couldn’t be opened error and write the (new) data to disk or check if the file exists with fileExists(atPath.
And you can declare the return value in pListURL as non-optional. It's guaranteed that the folder Documents exists
func pListURL() -> URL {
return try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("somePlist.plist")
}
Update:
These are reliable versions of update and write
func update(dictionary: [String:Int]) {
let url = pListURL()
guard FileManager.default.fileExists(atPath: url.path) else { write(dictionary: dictionary); return }
do {
let dataContent = try Data(contentsOf: url)
if var dict = try PropertyListSerialization.propertyList(from: dataContent, format: nil) as? [String: Int] {
for (key, value) in dictionary {
dict.updateValue(value, forKey: key)
}
write(dictionary: dict)
} else {
write(dictionary: dictionary)
}
} catch {
print(error)
}
}
func write(dictionary: [String:Int]) {
let url = pListURL()
do {
let plistData = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0)
try plistData.write(to: url)
} catch {
print(error)
}
}
Trying to create a file using createFile API on iOS.
createFile(path, data, attr)
Document says,
Return Value:
true if the operation was successful or if the item already exists, otherwise false.
I wonder if the operation was not successful, how can I get the exact cause of the failure?. Expected some API like,
createFile(path, data, attr, &error)
Use Data.write(to:options:) instead, it throws.
I use FileManager.default.createDirectory to create folders. I create Data that I want to write, and then I use Data.write to save it to disk.
var docDir = URL()
do {
docDir = try FileManager.default.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
} catch {
fatalError("\(error)")
}
let subDir = docDir.appendPathComponents("mySubDir")
do {
try FileManager.default.createDirectory(
at: subdir,
withIntermediateDirectories: true,
attributes: nil
)
} catch {
if (error as NSError).code == 516 {
// "The file “x” couldn’t be saved in the folder “y” because a file with the same name already exists."
// This is OK.
} else {
fatalError("\(error)")
}
}
let filePath = subdir.appendingPathComponent("yourFileName")
let object:Codable = Whatever()
let data = try! JSONEncoder().encode(object)
do {
try data.write(to: filePath)
} catch {
fatalError("\(error)")
}
I do not know why Apple devs chose FileManager.createFile to be different. It seems strange to me.
I am developing a simple app for iOS.
And I was really surprised that when image is written like in the code below, the path stays valid before the app restarts. After the app restarts the new sandbox is created and this invalidates the previous path. I am trying to find a way to have sandbox independent file path. Is it even possible without going through the resolution cycle (i.e. FileManager.default.url...)?
static func saveToDocument(name: String, image: UIImage) -> String? {
guard let data = image.pngData() else { return nil; }
guard let directory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) as NSURL else {
return nil
}
do {
let file = directory.appendingPathComponent("\(name).png");
try data.write(to: file!)
return file?.absoluteString
} catch {
return nil
}
}
Stop searching, there is no way, and there is a good reason for that: The safety of your data.
You always have to get the URL of the documents directory with FileManager.default.url(for, it's not that expensive.
Guarding the URL is not necessary, the documents folder is guaranteed to exist.
Return only the file name rather than the complete URL string and I recommend to make the function throw to hand over all errors.
enum FileError : Error {
case noData
}
static func saveToDocument(name: String, image: UIImage) throws -> String {
guard let data = image.pngData() else { throw FileError.noData }
let directory = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = directory.appendingPathComponent("\(name).png")
try data.write(to: fileURL)
return fileURL.lastPathComponent
}
And never use NS... classes like NSURL in Swift if there is a native counterpart (URL).
I'm using FMdatabase.
I want to use a prepared database.
I think I should move database file from bundle to documents folder.
my code:
import FMDB
class DatabaseManager {
private let dbFileName = "kashanmapDB_upgrade_3-4.db"
private var database:FMDatabase!
let TABLE_LOCATION_FA = "LocationInfoFa";
let TABLE_LOCATION_EN = "LocationInfoEn";
let TABLE_GREAT_PEOPLE_FA = "GreatPeopleInfoFa";
let TABLE_GREAT_PEOPLE_EN = "GreatPeopleInfoEn";
let TABLE_TAGS = "Tags";
let TABLE_RELATION_TAG_LOCATION = "RelationTagLocation";
let TABLE_NECESSARY_INFORMATION = "NecessaryInformation";
let TABLE_SLIDER_FA = "SliderFa";
let TABLE_SLIDER_EN = "SliderEn";
let DATABASE_VERSION = 4;
static var LANGUAGE = 1 ; //1:Fa , 2:En
var utilities = Utilities()
init() {
openDatabase()
if(utilities.getData(key: "lang") == "2")
{
DatabaseManager.LANGUAGE = 2
}
}
func copyDatabaseIfNeeded() {
// Move database file from bundle to documents folder
let fileManager = FileManager.default
let documentsUrl = fileManager.urls(for: .documentDirectory,
in: .userDomainMask)
guard documentsUrl.count != 0 else {
return // Could not find documents URL
}
//let finalDatabaseURL = documentsUrl.first!.appendingPathComponent("kashanmapDB_upgrade_3-4.db")
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let finalDatabaseURL = URL(fileURLWithPath: paths).appendingPathComponent(dbFileName)
if !( (try? finalDatabaseURL.checkResourceIsReachable()) ?? false) {
print("DB does not exist in documents folder")
let documentsURL = Bundle.main.resourceURL?.appendingPathComponent("kashanmapDB_upgrade_3-4.db")
do {
try fileManager.copyItem(atPath: (documentsURL?.path)!, toPath: finalDatabaseURL.path)
} catch let error as NSError {
print("Couldn't copy file to final location! Error:\(error.description)")
}
} else {
print("Database file found at path: \(finalDatabaseURL.path)")
}
}
func openDatabase() {
self.copyDatabaseIfNeeded()
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let dbPath = URL(fileURLWithPath: paths).appendingPathComponent(dbFileName)
let str_path = Bundle.main.resourceURL!.appendingPathComponent(dbFileName).path
let database = FMDatabase(path: str_path)
/* Open database read-only. */
if (!(database.open(withFlags: 2))) {
print("Could not open database at \(dbPath).")
} else {
print("opened database")
self.database = database;
}
}
at the first time (when application installed ) I got this error message:
DB does not exist in documents folder
and I always got this message:
Error Domain=FMDatabase Code=8 "attempt to write a readonly database" UserInfo={NSLocalizedDescription=attempt to write a readonly database}
Hmmm... Looking at your code:
func openDatabase() {
self.copyDatabaseIfNeeded()
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let dbPath = URL(fileURLWithPath: paths).appendingPathComponent(dbFileName)
let str_path = Bundle.main.resourceURL!.appendingPathComponent(dbFileName).path
let database = FMDatabase(path: str_path)
/* Open database read-only. */
if (!(database.open(withFlags: 2))) {
print("Could not open database at \(dbPath).")
} else {
print("opened database")
self.database = database;
}
}
It appears you are setting dbPath equal to the path to the file in documents folder, but then you're trying to open database which is at str_path which is equal to the Bundle path.
Maybe just change:
let database = FMDatabase(path: str_path)
to:
let database = FMDatabase(path: dbPath)
Having copied the database, you are trying to open the database from the bundle. Open the one in the Documents folder. If you define the bundle URL inside the if statement that handles the missing database (like shown below), there's no possibility of accidentally grabbing the wrong database.
As an aside, Apple is getting more particular about what gets stored in Documents folder (see iOS Storage Best Practices). You might want to use Application Support folder instead.
let fileURL = try FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("test.sqlite")
let fileExists = (try? fileURL.checkResourceIsReachable()) ?? false
if !fileExists {
let bundleURL = Bundle.main.url(forResource: "test", withExtension: "sqlite")!
try FileManager.default.copyItem(at: bundleURL, to: fileURL)
}
let db = FMDatabase(url: fileURL)
guard db.open() else {
print("unable to open")
return
}
Alternatively, it’s often preferred to adopt the “ask for forgiveness rather than permission” strategy. I.e., rather than checking for existence before you open the database every time you open it, just try to open it and handle the file-not-found error scenario (which will happen just once, the first time you try opening it). Bottom line, just try opening the database, and if it fails, copy it from the bundle and try again.
The trick is to supply the SQLITE_OPEN_READWRITE parameter (made available if you import SQLite3) but not the SQLITE_OPEN_CREATE so that it won’t create a blank database if it’s not found that first time you try opening it:
let fileURL = try FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("test.sqlite")
let db = FMDatabase(url: fileURL)
if !db.open(withFlags: SQLITE_OPEN_READWRITE) {
let bundleURL = Bundle.main.url(forResource: "test", withExtension: "sqlite")!
try FileManager.default.copyItem(at: bundleURL, to: fileURL)
guard db.open(withFlags: SQLITE_OPEN_READWRITE) else {
print("unable to open")
return
}
}
I'm using FMDatabase library to used prepped database sqlite.
I got this error:
Error Domain=FMDatabase Code=8 "attempt to write a readonly database" UserInfo={NSLocalizedDescription=attempt to write a readonly database}
2017-10-27 19:59:10.983238+0330 kashanmap[417:63718] Unknown error calling sqlite3_step (8: attempt to write a readonly database) eu
2017-10-27 19:59:10.983473+0330 kashanmap[417:63718] DB Query: insert into LocationInfoFa
it's my class:
import FMDB
class DatabaseManager {
private let dbFileName = "kashanmapDB_upgrade_3-4.db"
private var database:FMDatabase!
let TABLE_LOCATION_FA = "LocationInfoFa";
let TABLE_LOCATION_EN = "LocationInfoEn";
let TABLE_GREAT_PEOPLE_FA = "GreatPeopleInfoFa";
let TABLE_GREAT_PEOPLE_EN = "GreatPeopleInfoEn";
let TABLE_TAGS = "Tags";
let TABLE_RELATION_TAG_LOCATION = "RelationTagLocation";
let TABLE_NECESSARY_INFORMATION = "NecessaryInformation";
let TABLE_SLIDER_FA = "SliderFa";
let TABLE_SLIDER_EN = "SliderEn";
let DATABASE_VERSION = 4;
static var LANGUAGE = 1 ; //1:Fa , 2:En
var utilities = Utilities()
init() {
openDatabase()
if(utilities.getData(key: "lang") == "2")
{
DatabaseManager.LANGUAGE = 2
}
}
func openDatabase() {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let dbPath = URL(fileURLWithPath: paths).appendingPathComponent(dbFileName)
let str_path = Bundle.main.resourceURL!.appendingPathComponent(dbFileName).path
let database = FMDatabase(path: str_path)
/* Open database read-only. */
if (!(database.open(withFlags: 2))) {
print("Could not open database at \(dbPath).")
} else {
print("opened database")
self.database = database;
}
}
func closeDatabase() {
if (database != nil) {
database.close()
}
}
path of my database:
my query:
do {
let db = database
let q = try db?.executeUpdate("insert into \(table) (catid,subcat_id,id,subcat_title,title,description,lat,lon,takhfif,images,wifi,apple_health,wc,full_time,pos,work_hours,phone,mobile,fax,website,email,address,facebook,instagram,linkedin,telegram,googleplus,twitter,publish,feature,manager,city,rating_sum,rate_count,lastip,parking,isMallID,mallID,discount_images,price_images,newProduct_images,services_images,order_online,out_upon,cat_title,cat_icon,last_modify,item_logo,cat_logo,rate_sum1,rate_sum2,rate_sum3,rate_count1,rate_count2,rate_count3,rate_title1,rate_title2,rate_title3,rate_enable,installments_text,installments_image) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", values:[ catid,subcat_id,id,subcat_title,title,description,lat,lon,takhfif,images,wifi,apple_health,wc,full_time,pos,work_hours,phone,mobile,fax,website,email,address,facebook,instagram,linkedin,telegram,googleplus,twitter,publish,feature,manager,city,rating_sum,rate_count,lastip,parking,isMallID,mallID,discount_images,price_images,newProduct_images,services_images,order_online,out_upon,cat_title,cat_icon,last_modify,item_logo,cat_logo,rate_sum1,rate_sum2,rate_sum3,rate_count1,rate_count2,rate_count3,rate_title1,rate_title2,rate_title3,rate_enable,installments_text,installments_image])
} catch {
print("\(error)")
}
there are some solutions in stack overflow but them don't accepted as true answer.
updated2
I got this error:
DB does not exist in documents folder
my code:
init() {
openDatabase()
if(utilities.getData(key: "lang") == "2")
{
DatabaseManager.LANGUAGE = 2
}
}
func copyDatabaseIfNeeded() {
// Move database file from bundle to documents folder
let fileManager = FileManager.default
let documentsUrl = fileManager.urls(for: .documentDirectory,
in: .userDomainMask)
guard documentsUrl.count != 0 else {
return // Could not find documents URL
}
let finalDatabaseURL = documentsUrl.first!.appendingPathComponent("kashanmapDB_upgrade_3-4.db")
if !( (try? finalDatabaseURL.checkResourceIsReachable()) ?? false) {
print("DB does not exist in documents folder")
let documentsURL = Bundle.main.resourceURL?.appendingPathComponent("kashanmapDB_upgrade_3-4.db")
do {
try fileManager.copyItem(atPath: (documentsURL?.path)!, toPath: finalDatabaseURL.path)
} catch let error as NSError {
print("Couldn't copy file to final location! Error:\(error.description)")
}
} else {
print("Database file found at path: \(finalDatabaseURL.path)")
}
}
func openDatabase() {
self.copyDatabaseIfNeeded()
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let dbPath = URL(fileURLWithPath: paths).appendingPathComponent(dbFileName)
let str_path = Bundle.main.resourceURL!.appendingPathComponent(dbFileName).path
let database = FMDatabase(path: str_path)
/* Open database read-only. */
if (!(database.open(withFlags: 2))) {
print("Could not open database at \(dbPath).")
} else {
print("opened database")
self.database = database;
}
}
You are facing this error because you are trying to write (update) the .db file in the bundle directory which is not allowed:
AppName.app:
This is the app’s bundle. This directory contains the app and all of
its resources. You cannot write to this directory. To prevent
tampering, the bundle directory is signed at installation time.
Writing to this directory changes the signature and prevents your app
from launching. You can, however, gain read-only access to any
resources stored in the apps bundle.
File System Basics - Table 1-1: Commonly used directories of an iOS app
If you are aiming to update the file, you should implement a logic to copy it -if it's not already has been copied before- into the documents directory, thus you'd be able to read/write transactions with the copied file.
Remark that for iOS 11 and above, you might want to copy the database file into the Application Support directory if you don't want to let be viewable to the end users when navigating to your app by the Files iOS app. For details, check the iOS Storage Best Practices Apple video session.
You'd notice that this logic should be applied to any file in the app main bundle, for instance it is also applicable for JSON files.