How to solve Core Data error: Attempt to add read-only file at path when trying to access Core Data container in the app extension today widget? - ios

I am trying to access a database using Core Data inside a today extension widget.
The function that creates the Core Data container is this:
func createContainer(completion: #escaping (NSPersistentContainer) -> ()) {
let applicationGroupIdentifier = "group.com.name.exampleApp.sharedContainer"
guard let newURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: applicationGroupIdentifier)
else { fatalError("Could not create persistent store URL") }
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.url = newURL
let container = NSPersistentContainer(name: "ContainerName")
container.persistentStoreDescriptions = [persistentStoreDescription]
container.loadPersistentStores { (_, error) in
guard error == nil else { fatalError("Failed to load store:\(String(describing: error))") }
DispatchQueue.main.async { completion(container) }
}
}
However, I get the following error:
CoreData: error: Attempt to add read-only file at path
file:///private/var/mobile/Containers/Shared/AppGroup/C9324B65B-265C-4264-95DE-B5AC5C9DAFD0/
read/write. Adding it read-only instead. This will be a hard error in
the future; you must specify the NSReadOnlyPersistentStoreOption.
If I specify the NSReadOnlyPersistentStoreOption like this:
persistentStoreDescription.isReadOnly = false
I get the error:
Failed to load store:Optional(Error Domain=NSCocoaErrorDomain Code=513
"The file couldn’t be saved because you don’t have permission."):
What might be the cause and the solution?

Had the same problem and solved it by adding
let directory = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.identifier")!
let url = directory.appendingPathComponent("MyDatabaseName.sqlite")
CoreData probably tried to open the base folder as its sqlite database, which did not go so well.
Adding isReadOnly didn't allow CoreData to repurpose the Folder either :)
Source: https://github.com/maximbilan/iOS-Shared-CoreData-Storage-for-App-Groups

In extension's info.plist set: NSExtension --> NSExtensionAttributes --> RequestOpenAccess --> YES
In Settings on device set: Your app --> (Extension type - eg.: Keyboard) --> Allow Full Access

Related

How to include a read-write (existing) SQLite database in an iOS app from Xcode?

I'm trying to include a pre-created sqlite database from Xcode in my iOS app. The database needs to be updated when the app receives certain notifications (in JSON format) from a web server.
The database is included in the Xcode files and I see it inside the inspector.
The function I'm using to insert data inside my database is the following:
Import SQLite
func insertItem(item: Item){
let id = Expression<Int64>("id")
let description = Expression<String>("description")
let bundle = Bundle.main
let path = bundle.path(forResource: "DatabaseLocale", ofType: "sqlite")!
let db = try! Connection("\(path)")
let items = Table("Items")
let insert = items.insert(id <- Int64(item.id), description <- item.description)
do{
let rowid = try! db.run(insert)
} catch {
print("cannot modify the database")
}
do{
for itemID in try db.prepare(items){
print(itemID[id])
print(itemID[description])
}
} catch {
print("couldn't connect to database")
}
}
Now, when I run the app inside the simulator, I get the result I want, so the new data the app receives is correctly written inside the database, but whenever I use my physical iPhone, I get an error:
Fatal error: 'try!' expression unexpectedly raised an error: attempt to write a readonly database (code: 8)
How do I make the SQLite database inside the iPhone app modifiable?

Unable to destroy persistent store created with Core Data and SQLite

I have an iOS app where I want to start with a fresh Core Data database on every launch. The store type is SQLite.
However, when I call persistentStoreCoordinator.destroyPersistentStore(), I get an error 100% of the time.
Here is the code:
func destroyPersistentStore() {
guard let modelURL = Bundle.main.url(forResource: self.modelName, withExtension: "momd") else {
print("Missing data model - could not destroy")
return
}
do {
try persistentStoreCoordinator.destroyPersistentStore(at: modelURL, ofType: storeType, options: nil)
} catch {
print("Unable to destroy persistent store: \(error) - \(error.localizedDescription)")
}
}
The error is:
Unable to destroy persistent store: Error Domain=NSSQLiteErrorDomain
Code=14 "(null)" UserInfo={NSFilePath=.../AppName.app/ModelName.momd,
reason=Failed to truncate database} - The operation couldn’t be
completed. (NSSQLiteErrorDomain error 14.)
Even after this error, the app is able to save and access data in the store. The problem is that the initial data is being loaded on each launch, creating duplicates.
Here is the situation at the point where the call to destroyPersistentStore takes place:
The SQLite data file definitely exists and contains data
Happens on simulator or real device
The modelUrl is correct and points to the momd
Store type is SQLite
SQLite data file is saved in Documents directory
persistentStoreCoordinator.url(for: persistentStoreCoordinator.persistentStores.first!) is pointing to the file in the Documents directory.
I've searched online for answers and can't find anyone reporting this error, but I have the error in both this project and a simplified demo project. I cannot make destroyPersistentStore work at all.
Lastly, when I pause execution and po the persistentStoreCoordinator.managedObjectModel, the first line is:
po persistentStoreCoordinator.managedObjectModel
() isEditable 0, entities...
Could the isEditable issue be the problem? How would I change it?
You're conflating two objects in the Core Data stack:
The model is inside your app bundle, has the extension .momd, and contains information about your Core Data object definitions: what entities you have, what properties they have, their relationships, and so on.
The persistent store is a data file in your app's container (not in the bundle). You define its URL when you create or load persistent stores. It contains data for actual instances of model objects, rather than abstract definitions.
Rather than getting the URL of your model, I think you want to get the URL of a persistent store. You can do that by looking at the persistent store coordinator's persistentStores array, picking one, and getting its URL:
func destroyPersistentStore() {
guard let firstStoreURL = persistentStoreCoordinator.persistentStores.first?.url else {
print("Missing first store URL - could not destroy")
return
}
do {
try persistentStoreCoordinator.destroyPersistentStore(at: firstStoreURL, ofType: storeType, options: nil)
} catch {
print("Unable to destroy persistent store: \(error) - \(error.localizedDescription)")
}
}
This would destroy the first store; if you have multiple, you could instead loop over the persistent stores destroying them all, depending on your app's requirements.
iOS 15 version
// function to delete persistent store
func deletePersistentStore() {
let coordinator = self.persistentContainer.persistentStoreCoordinator
guard let store = coordinator.persistentStores.first else {
return
}
let storeURL = coordinator.url(for: store)
do {
if #available(iOS 15.0, *) {
let storeType: NSPersistentStore.StoreType = inMemoryStore ? .inMemory : .sqlite
try coordinator.destroyPersistentStore(at: storeURL, type: storeType)
} else {
let storeType: String = inMemoryStore ? NSInMemoryStoreType : NSSQLiteStoreType
try coordinator.destroyPersistentStore(at: storeURL, ofType: storeType)
}
}
catch {
print(error.localizedDescription)
}
}
Found this great solution:
import Foundation
import CoreData
extension NSManagedObjectContext
{
func deleteAllData()
{
guard let persistentStore = persistentStoreCoordinator?.persistentStores.last else {
return
}
guard let url = persistentStoreCoordinator?.url(for: persistentStore) else {
return
}
performAndWait { () -> Void in
self.reset()
do
{
try self.persistentStoreCoordinator?.remove(persistentStore)
try FileManager.default.removeItem(at: url)
try self.persistentStoreCoordinator?.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
}
catch { /*dealing with errors up to the usage*/ }
}
}
}

Backup Realm to iCloud Drive

I would like to backup a realm database file to an iCloud drive, like WhatsApp, I have some questions:
What is the best practice to do this?
I have a database located in a shared group folder to access it from extensions, how can I back it up? How can I show the progress bar of upload? Like WhatsApp for example?
If I put a realm file in a document folder it will be synced for each modify.
Are there some samples code that we can see?
Thanks for the help, have any ideas? links?
Just to clarify, this is a question about backing up a discrete Realm file itself to iCloud Drive, so that it would be visible in the iCloud Drive app. Not synchronizing the contents of the file to a CloudKit store.
If you leave the Realm file in the Documents directory, then if the user performs an iCloud or iTunes backup, the file will be backed up. All this means though is that if the user decides to upgrade to a new device and perform a restore using the old device's backup image, the Realm file will be restored then. If the user deletes the app from your old device before then, the iCloud backup will also be deleted.
If you want to export your Realm file so it can be permanently saved and accessed in iCloud Drive, you can export a copy of the Realm file to your app's iCloud ubiquity container. This is basically just another folder like the shared group's folder, but it's managed by iCloud. This folder sort of behaves like Dropbox in that anything you put in there is automatically synchronized.
The code would look something like this:
let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)
let realmArchiveURL = containerURL.appendPathComponent("MyArchivedRealm.realm")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL)
This is a really basic example. The Apple documentation recommends you do this on a background thread since setting up the iCloud folder for the first time can create some time.
Updating this wouldn't happen automatically. You'll need to export a new copy of the Realm each time the user wants to perform a backup.
I have recently had the same requirements and I am able to achieve from below steps
Swift: 4+
Step:1
1.Setup Your cloudKit for your app with a Developer account
2. You can take reference: https://www.raywenderlich.com/1000-cloudkit-tutorial-getting-started
Step 2
- Add CloudKit Capabilities in your App
- Please check out the screenshot: https://prnt.sc/pdpda5
Step 3
- Check for cloud Enabled options for your iphone
// Return true if iCloud is enabled
func isCloudEnabled() -> Bool {
if DocumentsDirectory.iCloudDocumentsURL != nil { return true }
else { return false }
}
Step 4
- Setup the below variables for Local or iCloud Document directories
struct DocumentsDirectory {
static let localDocumentsURL = FileManager.default.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: .userDomainMask).last!
static let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents")
}
Step:5
Below function is used for copyRealmFileToIcloudContainer
func uploadDatabaseToCloudDrive()
{
if(isCloudEnabled() == false)
{
self.iCloudSetupNotAvailable()
return
}
let fileManager = FileManager.default
self.checkForExistingDir()
let iCloudDocumentsURL = FileManager.default.url(forUbiquityContainerIdentifier: nil)?.appendingPathComponent("Documents", isDirectory: true)
let iCloudDocumentToCheckURL = iCloudDocumentsURL?.appendingPathComponent("\(memberId)_default.realm", isDirectory: false)
let realmArchiveURL = iCloudDocumentToCheckURL//containerURL?.appendingPathComponent("MyArchivedRealm.realm")
if(fileManager.fileExists(atPath: realmArchiveURL?.path ?? ""))
{
do
{
try fileManager.removeItem(at: realmArchiveURL!)
print("REPLACE")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL!)
}catch
{
print("ERR")
}
}
else
{
print("Need to store ")
let realm = try! Realm()
try! realm.writeCopy(toFile: realmArchiveURL!)
}
}
Step:6
- Once your realm file uploaded on the server , you can check this in your iPhone
- Steps
- 1.Go To Setting
- 2.Go To iCloud
- 3.Go To ManageStorage
- 4.You will see your application there
- 5.Tap on Application, you will able to see your realm file over there
Step:7
- Make Sure you have added the below lines in info.plist
<key>NSUbiquitousContainers</key>
<dict>
<key>iCloud.com.example.app</key>
<dict>
<key>NSUbiquitousContainerIsDocumentScopePublic</key>
<true/>
<key>NSUbiquitousContainerName</key>
<string>iCloudDemoApp</string>
<key>NSUbiquitousContainerSupportedFolderLevels</key>
<string>Any</string>
</dict>
</dict>
#yonlau as per your request sharing answer for backup realm file , This is tested once and the realm data only have when they backup on iCloud.
func DownloadDatabaseFromICloud()
{
let fileManager = FileManager.default
// Browse your icloud container to find the file you want
if let icloudFolderURL = DocumentsDirectory.iCloudDocumentsURL,
let urls = try? fileManager.contentsOfDirectory(at: icloudFolderURL, includingPropertiesForKeys: nil, options: []) {
// Here select the file url you are interested in (for the exemple we take the first)
if let myURL = urls.first {
// We have our url
var lastPathComponent = myURL.lastPathComponent
if lastPathComponent.contains(".icloud") {
// Delete the "." which is at the beginning of the file name
lastPathComponent.removeFirst()
let folderPath = myURL.deletingLastPathComponent().path
let downloadedFilePath = folderPath + "/" + lastPathComponent.replacingOccurrences(of: ".icloud", with: "")
var isDownloaded = false
while !isDownloaded {
if fileManager.fileExists(atPath: downloadedFilePath) {
isDownloaded = true
print("REALM FILE SUCCESSFULLY DOWNLOADED")
self.copyFileToLocal()
}
else
{
// This simple code launch the download
do {
try fileManager.startDownloadingUbiquitousItem(at: myURL )
} catch {
print("Unexpected error: \(error).")
}
}
}
// Do what you want with your downloaded file at path contains in variable "downloadedFilePath"
}
}
}
}
2.Copy realm file from iCloud to Document directory
func copyFileToLocal() {
if isCloudEnabled() {
deleteFilesInDirectory(url: DocumentsDirectory.localDocumentsURL)
let fileManager = FileManager.default
let enumerator = fileManager.enumerator(atPath: DocumentsDirectory.iCloudDocumentsURL!.path)
while let file = enumerator?.nextObject() as? String {
do {
try fileManager.copyItem(at: DocumentsDirectory.iCloudDocumentsURL!.appendingPathComponent(file), to: DocumentsDirectory.localDocumentsURL.appendingPathComponent(file))
print("Moved to local dir")
//HERE ACCESSING DATA AVAILABLE IN REALM GET FROM ICLOUD
let realm = RealmManager()
let array = realm.FetchObjects(type: Mood.self)
print(array?.count)
} catch let error as NSError {
print("Failed to move file to local dir : \(error)")
}
}
}
}
You could take a look at this Github project by mikemac8888.
Basically you make your model objects conform to RealmCloudObject:
class Note: Object, RealmCloudObject {
...
}
You have to implement a mapping function :
func toRecord() -> CKRecord {
...
record["text"] = self.text
record["dateModified"] = self.dateModified
}
... and the reverse function used to create Realm records out of CloudKit records:
public func changeLocalRecord(...) throws {
...
realm.create(objectClass as! Object.Type,
value: ["id": id,
"text": text,
"dateModified": NSDate(),
"ckSystemFields": recordToLocalData(record)],
update: true)
...
}
The full documentation could be read at the link I provided, obviously.

Firebase Storage download to local file error

I'm trying to download the image f5bd8360.jpeg from my Firebase Storage.When I download this image to memory using dataWithMaxSize:completion, I'm able to download it.
My problem comes when I try to download the image to a local file using the writeToFile: instance method. I'm getting the following error:
Optional(Error Domain=FIRStorageErrorDomain Code=-13000 "An unknown
error occurred, please check the server response."
UserInfo={object=images/f5bd8360.jpeg,
bucket=fir-test-3d9a6.appspot.com, NSLocalizedDescription=An unknown
error occurred, please check the server response.,
ResponseErrorDomain=NSCocoaErrorDomain, NSFilePath=/Documents/images,
NSUnderlyingError=0x1700562c0 {Error Domain=NSPOSIXErrorDomain Code=1
"Operation not permitted"}, ResponseErrorCode=513}"
Here is a snippet of my Swift code:
#IBAction func buttonClicked(_ sender: UIButton) {
// Get a reference to the storage service, using the default Firebase App
let storage = FIRStorage.storage()
// Get reference to the image on Firebase Storage
let imageRef = storage.reference(forURL: "gs://fir-test-3d9a6.appspot.com/images/f5bd8360.jpeg")
// Create local filesystem URL
let localURL: URL! = URL(string: "file:///Documents/images/f5bd8360.jpeg")
// Download to the local filesystem
let downloadTask = imageRef.write(toFile: localURL) { (URL, error) -> Void in
if (error != nil) {
print("Uh-oh, an error occurred!")
print(error)
} else {
print("Local file URL is returned")
}
}
}
I found another question with the same error I'm getting but it was never answered in full. I think the proposal is right. I don't have permissions to write in the file. However, I don't know how gain permissions. Any ideas?
The problem is that at the moment when you write this line:
let downloadTask = imageRef.write(toFile: localURL) { (URL, error) -> Void in
etc.
you don't yet have permission to write to that (localURL) location. To get the permission you need to write the following code before trying to write anything to localURL
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let localURL = documentsURL.appendingPathComponent("filename")
By doing it you will write the file into the following path on your device (if you are testing on the real device):
file:///var/mobile/Containers/Data/Application/XXXXXXXetc.etc./Documents/filename
If you are testing on the simulator, the path will obviously be somewhere on the computer.

Swift: copying files gives Cocoa Error 262

I'm trying to move my files up a level in some random directory in the default Documents directory. But when copying, Swift gave me the error Cocoa Error 262: NSFileReadUnsupportedSchemeError. Why did I get this error? I ran a quick search and pretty much everyone who'd encountered this problem was trying to copy files out of the camera roll, but I'm not. Any thoughts? Thanks in advance.
Code here:
private func copyFilesInDirectory(fromDir: String, toDirectory toDir: String, withCompletionHandler handler: ()->()){
println("from: \(fromDir)\nto: \(toDir)")
var error: NSError?
var contents = fileManager.contentsOfDirectoryAtPath(fromDir, error: &error)
if let dirContents = contents{
let enumerator = (dirContents as NSArray).objectEnumerator()
while let file = enumerator.nextObject() as? String{
let filePath = fromDir.stringByAppendingString("/\(file)")
println("copying \(filePath)")
if(fileManager.copyItemAtURL(NSURL(fileURLWithPath: filePath)!, toURL: NSURL(string: toDir)!, error: &error)){
println("COPIED")
}
else{
println("COPY ERROR: \(error!.localizedDescription)")
}
}
handler()
}
}
and here's the log if anyone's interested:
from: /Users/dolce/Library/Developer/CoreSimulator/Devices/05566649-BABB-44BE-BE6F-AFF7B41E3065/data/Containers/Data/Application/F5A83C94-C177-4826-BDD5-3A50E7508239/Documents/Hello/tmp
to: /Users/dolce/Library/Developer/CoreSimulator/Devices/05566649-BABB-44BE-BE6F-AFF7B41E3065/data/Containers/Data/Application/F5A83C94-C177-4826-BDD5-3A50E7508239/Documents/Hello
copying /Users/dolce/Library/Developer/CoreSimulator/Devices/05566649-BABB-44BE-BE6F-AFF7B41E3065/data/Containers/Data/Application/F5A83C94-C177-4826-BDD5-3A50E7508239/Documents/Hello/tmp/test.png
COPY ERROR: The operation couldn’t be completed. (Cocoa error 262.)
The toURL parameter of copyItemAtURL() must not be the destination
directory, but the URL of the destination file. This can be
created with
let destPath = toDir.stringByAppendingPathComponent(file)
You should also replace
let filePath = fromDir.stringByAppendingString("/\(file)")
with
let filePath = fromDir.stringByAppendingPathComponent(file)
There is also a copyItemAtPath() method which saves you from
converting the paths to URLs.
On an iOS device, the simple answer is you can't do that. On the simulator things are little different. What you're probably dealing with is file permissions on the directory you're trying to write to.
As a general rule on iOS, you can write to the designated directories like the documents/caches and nothing else.

Resources