Save a copy of sqlite file in Core Data - ios

I am trying to make a copy of current Core Data database file.
After doing some research, I found that migratePersistentStore(_:to:options:withType:) might be useful in this situation, as the Apple documentation saying,
This method is typically used for “Save As” operations.
While the documentation also said,
Important
After invocation of this method, the specified store is removed from the coordinator thus store is no longer a useful reference.
Therefore, I tried to keep the original store reference so that the app can continue working.
Inspired by this thread, I followed Tom Harrington idea:
Create a new migrate-only NSPersistentStoreCoordinator and add your original persistent store file.
Use this new PSC to migrate to a new file URL.
Drop all reference to this new PSC, don't use it for anything else.
Here's my codes, for sharing backup sqlite file using UIActivityViewController:
func shareDatabase() {
let backupFileName = "Backup.sqlite"
let backupFilePath = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library").appendingPathComponent("Application Support").appendingPathComponent(backupFileName)
let mainDataPSC = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.persistentStoreCoordinator
let migratePSC = NSPersistentStoreCoordinator(managedObjectModel: mainDataPSC.managedObjectModel)
let origStore = mainDataPSC.persistentStores.first!
try! migratePSC.migratePersistentStore(origStore, to: backupFilePath, options: [NSSQLitePragmasOption: ["journal_mode": "DELETE"]], withType: NSSQLiteStoreType)
let activityVC = UIActivityViewController(activityItems: [backupFilePath], applicationActivities: nil)
present(activityVC, animated: true, completion: nil)
}
Unfortunately, it still lose reference to the original store after migration, and app is not working normally afterward.
Is this the correct way to make copy of sqlite file?
How can I keep the original reference?
Thanks, everyone!

You didn't quite do what I meant in that other answer, but in fairness I might not have explained it as clearly as I should have. You're having this problem because you're still migrating origStore, so it's still getting removed and is still becoming invalid. Using the new coordinator isn't enough-- you have to be sure to leave the existing Core Data stack alone.
A fuller description of the process is:
Create a new NSPersistentStoreCoordinator-- which you're already doing.
Add your persistent store file to the coordinator (your migratePSC) by using addPersistentStore(ofType:configurationName:at:options:). This function returns an NSPersistentStore object.
Use the new coordinator (migratePSC) to migrate the new persistent store from step 2 to a new location (backupFilePath).
After this, the persistent store from step 2 will be invalid, but you're not using it anywhere else so that doesn't matter. Meanwhile your existing NSPersistentContainer and all of its properties are untouched and your app continues to work.

Related

Core data testing: force external storage on iOS

I am trying to test core data's External Storage. Is there a way to force core data to write the data to a file? Adding a large data/image does not seem to work.
https://stackoverflow.com/a/7926505/429763
func setupOnDiskTestStore() {
let mom = NSManagedObjectModel.mergedModel(from: [Bundle.main, Bundle(for: type(of: self))])
psc = NSPersistentStoreCoordinator(managedObjectModel: mom!)
let store = try! psc.addPersistentStore(ofType: NSSQLiteStoreType,
configurationName: nil,
at: storeURL(),
options: nil)
expect(store).notTo(beNil())
moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
moc.persistentStoreCoordinator = psc
}
There's no way to force it to use external storage. The checkbox says it's allowed, but there's no way to make it required. As noted in the page you link to, it's related to the size of the data, so larger data blobs will be stored externally.
For testing only-- you can inspect the contents of the external storage directory to see what's there. This is completely undocumented, so you can't rely on it in an app, but it might be useful for testing. The data goes in a hidden directory in the same directory as your persistent store file. For example if your persistent store is named MyData.sqlite and it's located in the application support directory (which is where NSPersistentContainer puts it, unless you tell it to use a different location), then the external storage (if any) will be in Application Support/.MyData_SUPPORT/_EXTERNAL_DATA/. There will be one file per externally stored data object.
You can't match the files to managed objects by name, because the file names are UUIDs and the UUIDs aren't available in code. But if you were to create a single new managed object with external storage enabled, and you then found that there was a single new file in that directory, you'd know that the new file corresponds to the new object.

Swift 3 updating core data model causes crash for external testers

I released an app using core data to store some important information.
Recently I decided to re-do my data model to bring it up to date and make it easier to use.
I added some entities to the data model, and it runs fine in the simulator - however when I released it to the beta testers as soon as it tries to do anything with core data it is crashing.
I did not create a new version of the data model.
I have read here and here about how to deal with this error but both answers reference code that I do not have anywhere in my app, but they seem to have built in - they also talk about lightweight data migration? A lot of the answers reference a NSPersistentStoreCoordinator, which I do not have/know how to implement.
The code I have in the app delegate dealing with the persistentContainer is:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "App_Name")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
The other answers also referenced crashes in the simulator which required re-installation - I don't think I got these, but I may not have noticed.
What is the best way for me to update my data-model such that my users won't get crashes?
EDIT:
I have updated the persistentContainer to this:
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "App_Name")
let myFileManager = FileManager()
do {
let docsurl = try myFileManager.url(for:.documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let myUrl = docsurl.appendingPathComponent("UserDataTA")
if try myFileManager.contentsOfDirectory(atPath: docsurl.path).contains("UserDataTA") == false {
try myFileManager.createDirectory(at: myUrl, withIntermediateDirectories: false, attributes: nil)
try container.persistentStoreCoordinator.addPersistentStore(ofType: NSInMemoryStoreType, configurationName: nil, at: myUrl, options: nil)
}
} catch {
print(error)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalErrorText = error.debugDescription
firstFuncs.errorAlert(error: fatalErrorText)
}
})
return container
}()
however now the error message is "Error Domain=NSCocoaErrorDomainCode=134140 'Persistent store migration failed, missing mapping model.'"
Your crash sounds like it's happening because your data model changed, and any existing data that exists on a device can't be mapped 1:1 to the new model. This is where data mapping and migration comes in. Apple's docs are a good place to start. The very high-level overview is:
Open your xcdatamodel or xcdatamodeld file in Xcode, then select "Add Model Version...".
If necessary, save your model as an xcdatamodeld file. (The d suffix indicates that it's versioned.)
Note that your xcdatamodeld file is really a folder, and it includes multiple xcdatamodel files. All versions after the first one will include a numeric version number in the filename.
Make your model changes in the new model version.
If the changes are relatively simple (such as renaming an entity or adding or removing attributes), then it's considered a lightweight migration, and core data can figure out how to do the migration itself.
If your changes are more involved (such as splitting an entity type into two), then you must create a mapping model.
Add the code to perform the migration at startup time.
Obviously there's a lot more to this process, but this may give you a decent start.

Realm migration inadequately documented. Can anyone clarify?

Performing migrations on a Realm database is poorly documented and the documentation seems to be out-of-date. There are two areas explaining how to migrate data:
-- The simple example on the Realm website: https://realm.io/docs/swift/latest/
-- A more detailed example in the Github examples section: https://github.com/realm/realm-cocoa/blob/master/examples/ios/swift-3.0/Migration/AppDelegate.swift
Neither of these examples adequately explain how to migrate data between schema versions. I've tried playing around with the examples and have yet to get any migrations working. As well, I've had problems with app crashes when upgrading to newer Realm versions without schema changes and data changes, which don't occur in the Simulator but occur when installing the app from TestFlight or the App Store.
Seems like the Realm documentation and examples detailing migrations are due for a refresh. My areas of interest are:
Upgrading to a newer Realm version without a schema change in the database. Unclear whether I should continue using the default.realm file generated with a previous version, or whether I need to re-generate the default.realm file using the newer Realm framework version.
Adding a new attribute to a Realm object.
New objects ("rows") added to an existing class without any schema change.
No schema changes to existing classes in the database, but addition of an entirely new class or classes.
Any combination of the above.
Thanks!
Sorry the docs are not sufficient. We appreciate the feedback and will use it to improve them. In the mean time, let me answer your questions:
You do not need to do anything when you upgrade the SDK. Sometimes, we upgrade the core database file format, but this migration happens automatically when you open the Realm (Realm()) so you don't have to worry about it.
When you add a new property to an object you can just follow this code snippet.
Nothing is needed in the migration block since this block is simply to apply data transformations between versions. All you need to do is increment the schemaVersion
// Inside your application(application:didFinishLaunchingWithOptions:)
let config = Realm.Configuration(
// Set the new schema version. This must be greater than the previously used
// version (if you've never set a schema version before, the version is 0).
schemaVersion: 1,
// Set the block which will be called automatically when opening a Realm with
// a schema version lower than the one set above
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// Nothing to do!
// Realm will automatically detect new properties and removed properties
// And will update the schema on disk automatically
}
})
// Tell Realm to use this new configuration object for the default Realm
Realm.Configuration.defaultConfiguration = config
// Now that we've told Realm how to handle the schema change, opening the file
// will automatically perform the migration
let realm = try! Realm()
Adding objects to a Realm does not affect the schema so a migration is not relevant.
This is the same as 2, you simply need to increment the schemaVersion but you don't have to do anything in the migration block since Realm handles everything. The migration block is for custom migration logic where, for example, you want to transform firstName and lastName from schemaVersion=0 to fullName when updating to schemaVersion=1. In this case, you could get the data from the old version and concatenate the strings into the new fullName property within the migration block.
Hope this helps!
Unfortunately, that doesn't work, at all. There is a block of code I had to add to iOS apps several months ago in order to get Realm to work, but it doesn't solve the migration problems I've experienced:
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("default-v1.realm")
Realm.Configuration.defaultConfiguration = config
// copy over old data files for migration
//let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
let defaultURL = Realm.Configuration.defaultConfiguration.fileURL!
//print("Realm.Configuration.defaultConfiguration.fileURL! = \(defaultURL)")
//let defaultParentURL = defaultURL.URLByDeletingLastPathComponent
if let realmURL = bundleURL("default-v1") {
do {
//print("Bundle location for default.realm = \(realmURL)")
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = URL(fileURLWithPath: path)
let filePath = url.appendingPathComponent("default-v1.realm").path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
print("FILE INSTALLED")
print("Documents location for default.realm = \(filePath)")
} else {
//print("FILE NOT INSTALLED, COPYING default.realm from bundle to Documents directory.")
try FileManager.default.copyItem(at: realmURL, to: defaultURL)
}
} catch {}
}
I suspect that a new default-v1.realm file may clobber an existing default-v1.realm file on a user's device, using this code with the "Nothing is needed" migration method.
Part of the confusion stems from the example migration code at https://github.com/realm/realm-cocoa/tree/master/examples/ios/swift-3.0/Migration where data from the v1 file is getting copied over to the v2 file. Regardless, I've tried the "Nothing is needed" code you've posted and the app doesn't find any Realm data then crashes.
It looks like I could splice the code you've posted into the code I've been using then perhaps that would solve the problem. Will look into that next week.
Thanks for your response!

Ios swift - storing uiimage in document directory and path in core data

I'm currently storing my images directly in core data and experiencing issues when having to retrieve a high quality images.
I was advised to store the images in the document directory and save a path in core-data as to not store the actual images there but simply the address of where I can go and find it.
Can someone push me in the right direction? Can't seem to find the appropriate guidance?
I was able to find the answers to all my questions in this youtube video -
https://www.youtube.com/watch?v=6vk4UrJR8WM
This is the github with the file that has instructions as to how to complete this - https://github.com/TDAbboud/WeightLogger-Images
Try Hanake cache: https://github.com/Haneke/Haneke
It worked really well for me personally with the same problem. It makes a sometimes painful task pretty easy.
Save an image in Document directory is easy this code is not optimized is just for give you a hint( you should manage errors, optionals etc). Suppose that you already have a valid UIImage instance called image and a valid image name String called imageName
let data = UIImagePNGRepresentation(image)
// Unwrapping the data optional
if let data = data {
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let path = (paths.first! as! NSString).stringByAppendingPathComponent(imageName)
data.writeToFile(path, options: [])
//Save the path into your managed object
}

Background Seeding/Loading iOS Core Data Store

My iOS 8.0 + app is essentially a dictionary app, presenting a read-only data set to the user in an indexed, easily navigable format. I have explored several strategies for loading the static data, and I have decided to ship the app with several JSON data files that are serialized and loaded into a Core Data store once when the app is first opened. The call to managedObjectContext.save(), therefore, will happen only once in the lifetime of the app, on first use.
From reading Apple's Core Data Programming Guide in the Mac Developer Library (updated Sept. 2015), I understand that Apple's recommended practice is to 1) separate the Core Data stack from the AppDelegate into a dedicated DataController object (which makes it seem odd that even in Xcode 7.2 the Core Data stack is still put in the AppDelegate by default, but anyway...); and
2) open (and, I assume, seed/load) the persistent store in a background thread with a dispatch_async block, like so :
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
//(get URL to persistent store here)
do {
try psc.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: storeURL, options: nil)
//presumably load the store from serialized JSON files here?
} catch { fatalError("Error migrating store: \(error)") }
}
I'm just getting started learning about concurrency and GCD, so my questions are basic:
1) If the data set is being loaded in a background thread, which could take some non-trivial time to complete, how does the initial view controller know when the data is finished loading so that it can fetch data from the ManagedObjectContext to display in a UITableView ?
2) Along similar lines, if I would like to test the completely loaded data set by running some fetches and printing debug text to the console, how will I know when the background process is finished and it's safe to start querying?
Thanks!
p.s. I am developing in swift, so any swift-specific tips would be tremendous.
Instead of trying to make your app import the read-only data on first launch (forcing the user to wait while the data is imported), you can import the data yourself, then add the read-only .sqlite file and data model to your app target, to be copied to the app bundle.
For the import, specify that the persistent store should use the rollback journaling option, since write-ahead logging is not recommended for read-only stores:
let importStoreOptions: [NSObject: AnyObject] = [
NSSQLitePragmasOption: ["journal_mode": "DELETE"],]
In the actual app, also specify that the bundled persistent store should use the read-only option:
let readOnlyStoreOptions: [NSObject: AnyObject] = [
NSReadOnlyPersistentStoreOption: true,
NSSQLitePragmasOption: ["journal_mode": "DELETE"],]
Since the bundled persistent store is read-only, it can be accessed directly from the app bundle, and would not even need to be copied from the bundle to a user directory.
Leaving aside whether loading a JSON at the first startup is the best option and that this question is four years old, the solution to your two questions is probably using notifications. They work from all threads and every listening class instance will be notified. Plus, you only need to add two lines:
The listener (your view controller or test class for question 2) needs to listen for notifications of a specific notification name:
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleMySeedNotification(_:)), name: "com.yourwebsite.MyCustomSeedNotificationName", object: nil)
where #objc func handleMySeedNotification(_ notification: Notification) is the function where you are going to implement whatever should happen when a notification is received.
The caller (your database logic) the sends the notification on successful data import. This looks like this:
NotificationCenter.default.post(name: "com.yourwebsite.MyCustomSeedNotificationName", object: nil)
This is enough. I personally like to use an extension to Notification.Name in order to access the names faster and to prevent typos. This is optional, but works like this:
extension Notification.Name {
static let MyCustomName1 = Notification.Name("com.yourwebsite.MyCustomSeedNotificationName1")
static let MyCustomName2 = Notification.Name("CustomNotificationName2")
}
Using them now becomes as easy as this: NotificationCenter.default.post(name: .MyCustomSeedNotificationName1, object: nil) and even has code-completion after typing the dot!

Resources