iOS 11+ How to Migrate Existing Core Data to Shared App Group for Use in Extension

iOS 11+ How to migrate existing Core Data to Shared App Group for use in extension?

I ended up getting it doing the following. The sqlite file was actually the name of my init plus .sqlite at the end.

+ (NSPersistentContainer*) GetPersistentContainer {
//Init the store.
NSPersistentContainer *_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"Test_App"];

//Define the store url that is located in the shared group.
NSURL* storeURL = [[[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.Test_App"] URLByAppendingPathComponent:@"Test_App.sqlite"];

//Determine if we already have a store saved in the default app location.
BOOL hasDefaultAppLocation = [[NSFileManager defaultManager] fileExistsAtPath: _persistentContainer.persistentStoreDescriptions[0].URL.path];

//Check if the store needs migration.
BOOL storeNeedsMigration = hasDefaultAppLocation && ![_persistentContainer.persistentStoreDescriptions[0].URL.absoluteString isEqualToString:storeURL.absoluteString];

//Check if the store in the default location does not exist.
if (!hasDefaultAppLocation) {
//Create a description to use for the app group store.
NSPersistentStoreDescription *description = [[NSPersistentStoreDescription alloc] init];

//set the automatic properties for the store.
description.shouldMigrateStoreAutomatically = true;
description.shouldInferMappingModelAutomatically = true;

//Set the url for the store.
description.URL = storeURL;

//Replace the coordinator store description with this description.
_persistentContainer.persistentStoreDescriptions = [NSArray arrayWithObjects:description, nil];
}

//Load the store.
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
//Check that we do not have an error.
if (error == nil) {
//Check if we need to migrate the store.
if (storeNeedsMigration) {
//Create errors to track migration and deleting errors.
NSError *migrateError;
NSError *deleteError;

//Store the old location URL.
NSURL *oldStoreURL = storeDescription.URL;

//Get the store we want to migrate.
NSPersistentStore *store = [_persistentContainer.persistentStoreCoordinator persistentStoreForURL: oldStoreURL];

//Set the store options.
NSDictionary *storeOptions = @{ NSSQLitePragmasOption : @{ @"journal_mode" : @"WAL" } };

//Migrate the store.
NSPersistentStore *newStore = [_persistentContainer.persistentStoreCoordinator migratePersistentStore: store toURL:storeURL options:storeOptions withType:NSSQLiteStoreType error:&migrateError];

//Check that the store was migrated.
if (newStore && !migrateError) {
//Remove the old SQLLite database.
[[[NSFileCoordinator alloc] init] coordinateWritingItemAtURL: oldStoreURL options: NSFileCoordinatorWritingForDeleting error: &deleteError byAccessor: ^(NSURL *urlForModifying) {
//Create a remove error.
NSError *removeError;

//Delete the file.
[[NSFileManager defaultManager] removeItemAtURL: urlForModifying error: &removeError];

//If there was an error. Output it.
if (removeError) {
NSLog(@"%@", [removeError localizedDescription]);
}
}
];

//If there was an error. Output it.
if (deleteError) {
NSLog(@"%@", [deleteError localizedDescription]);
}
}
}
} else {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}];

//Return the container.
return _persistentContainer;
}

coredata - move to app group target

In case someone wants the solution in swift just add below function in didFinishLaunchingWithOptions.

 func migratePersistentStore(){

let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
var storeOptions = [AnyHashable : Any]()
storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true
storeOptions[NSInferMappingModelAutomaticallyOption] = true
let oldStoreUrl = self.applicationDocumentsDirectory.appendingPathComponent("YourApp.sqlite")!
let newStoreUrl = self.applicationGroupDirectory.appendingPathComponent("YourApp.sqlite")!
var targetUrl : URL? = nil
var needMigrate = false
var needDeleteOld = false

if FileManager.default.fileExists(atPath: oldStoreUrl.path){
needMigrate = true
targetUrl = oldStoreUrl
}

if FileManager.default.fileExists(atPath: newStoreUrl.path){
needMigrate = false
targetUrl = newStoreUrl

if FileManager.default.fileExists(atPath: oldStoreUrl.path){
needDeleteOld = true
}
}
if targetUrl == nil {
targetUrl = newStoreUrl
}
if needMigrate {
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: targetUrl!, options: storeOptions)
if let store = coordinator.persistentStore(for: targetUrl!)
{
do {
try coordinator.migratePersistentStore(store, to: newStoreUrl, options: storeOptions, withType: NSSQLiteStoreType)

} catch let error {
print("migrate failed with error : \(error)")
}
}
} catch let error {
CrashlyticsHelper.reportCrash(err: error as NSError, strMethodName: "migrateStore")
}
}
if needDeleteOld {
DBHelper.deleteDocumentAtUrl(url: oldStoreUrl)
guard let shmDocumentUrl = self.applicationDocumentsDirectory.appendingPathComponent("NoddApp.sqlite-shm") else { return }
DBHelper.deleteDocumentAtUrl(url: shmDocumentUrl)
guard let walDocumentUrl = self.applicationDocumentsDirectory.appendingPathComponent("NoddApp.sqlite-wal") else { return }
DBHelper.deleteDocumentAtUrl(url: walDocumentUrl)
}
}

My PersistentStoreCoordinator Looks like this:

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationGroupDirectory.appendingPathComponent("YourApp.sqlite")
var storeOptions = [AnyHashable : Any]()
storeOptions[NSMigratePersistentStoresAutomaticallyOption] = true
storeOptions[NSInferMappingModelAutomaticallyOption] = true
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options:storeOptions)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?

dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()

This is the case when there is already an app in appstore and you want to migrate the coreData persistent store file from your default store location to your App Group Location.

Edit : For Deleting the file from the old location it is recommended that we use NSFileCoordinator to perform the task.

static func deleteDocumentAtUrl(url: URL){
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: {
(urlForModifying) -> Void in
do {
try FileManager.default.removeItem(at: urlForModifying)
}catch let error {
print("Failed to remove item with error: \(error.localizedDescription)")
}
})
}

Please note the reason why NSFileCoordinator is used to delete the file is because NSFileCoordinator allows us to ensure that file related tasks such as opening reading writing are done in such a way that wont interfere with anyother task on the system trying to work with the same file.Eg if you want to open a file and at the same time it gets deleted ,you dont want both the actions to happen at the same time.

Please call the above function after the store is successfully migrated.

Core Data move data into shared container

You can migrate a Core Data Stack. A fuller answer can be found here, but the short version is:

  1. Check if the old non-group copy of the data exists
  2. If it does, set up a Core Data stack using that file. Then use migratePersistentStore:toURL:options:withType:error: to move it to the new location. Then remove the old copy.
  3. If the old version doesn't exist, just set up Core Data with the new copy as usual.

(The problem with Stephen's answer is that it assumes that the Core Data stack is a single SQLite file, which isn't always true.)

Migrating Data to App Groups Disables iCloud Syncing

If you are still facing the same problem, you should add the following line for the storeDescription

storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.yourapp.identifier")

Source: https://developer.apple.com/videos/play/wwdc2019/202/

Following is my CoreDataStack:

import CoreData

class CoreDataStack {
// MARK: - Core Data stack

static var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentCloudKitContainer(name: "data")

let storeURL = URL.storeURL(for: "group.com.myapp", databaseName: "data")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.myapp")
container.persistentStoreDescriptions = [storeDescription]

container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()


// MARK: - Core Data Saving support

static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}

public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}

return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}

Inter-App Data Migration (migrating data to new app version)

I would suggest using App Groups to get a shared container. I’m not familiar with Ionic, but this is quite straightforward in native Swift. It allows multiple apps or extensions to access a shared container of data, like the image below:

Sample Image

(Image from https://agostini.tech/2017/08/13/sharing-data-between-applications-and-extensions-using-app-groups/ )


This would require an update to the existing app to copy data to the shared container and then users would have to install the new app while the old one was still installed, because the shared container will be deleted when there are no installed apps using it.

It can be set up like this:

1: Enable App Groups in your project's Capabilities tab (for both apps).

2: Add a new app group and name it something like "group.appDomain.appName" or similar.

3: Now that the App Group is set up, it’s shared container can be used in several ways (User Defaults, NSCoding or Core Data).


For shared User Defaults:

let defaults = UserDefaults.init(suiteName: "group.appDomain.appName")
defaults.set("Example", forKey: "exampleKey")   
defaults.synchronize()

More info from Apple here.


For NSCoding:

let sharedContainerDirectory: URL = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.appDomain.appName")!

let sharedArchiveURL: URL = sharedContainerDirectory.appendingPathComponent("whateverYouNeed")

NSKeyedArchiver.archiveRootObject(yourObject, toFile: sharedArchiveURL.path)

For Core Data:

You can set up the container as below. I have taken this code from this answer as I have not actually tried this with Core Data myself.

You use containerURL(forSecurityApplicationGroupIdentifier: "group.appDomain.appName")! to make this work in shared container.

lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application.
This implementation creates and returns a container, having loaded the store for the application to it.
This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "xx")

let appName: String = "xx"
var persistentStoreDescriptions: NSPersistentStoreDescription

let storeUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.appDomain.appName")!.appendingPathComponent("xx.sqlite")

let description = NSPersistentStoreDescription()
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
description.url = storeUrl

container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.xxx.xx.container")!.appendingPathComponent("xx.sqlite"))]

container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()

This answer provides a way to migrate the persistent store.

As I mentioned, I am not familiar with Ionic, so I’m not sure how working in that context might change this technique.

I hope this is helpful.

App Groups forSecurityApplicationGroupIdentifier returns nil

I had the same issue in the past. To fix FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: id) being nil, just make sure that the entitlements that are being used for the debug (You can find this in your target's Build Settings) has the app group you made. Sometimes it's not added.

Update Core Data store location to support App Groups

A Core Data NSSQLiteStoreType store created with the default options is actually several files, as described in Technical Q&A 1809: New default journaling mode for Core Data SQLite stores in iOS 7 and OS X Mavericks. This is important to remember when attempting to move a store outside of a migration process, and is the source of your issue - you are moving one file when you need to be moving all of them. Moving the files individually outside of Core Data and without the benefits of a file coordinator is not recommended, however. It's much better to use a migration instead.

A migration will take the data from the source store and migrate it to your new store location, essentially replicating the old data at the new location. The old data will still exist on the filesystem. In your application, you should perform the migration as you are now, but do not attempt to move the old data to the new location yourself - that is where things are going wrong.

Instead of moving files around yourself, you can rely on a migration to move the data for you. First, add a store to the persistent store coordinator with the URL of the source data. Then you will perform a migration to move that data to the new URL

NSPersistentStore   *sourceStore        = nil;
NSPersistentStore *destinationStore = nil;
NSDictionary *storeOptions = @{ NSSQLitePragmasOption : @{ @"journal_mode" :
@"WAL" } };

// Add the source store
if (![coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:oldStoreURL options:storeOptions error:&error]){
// Handle the error
} else {
sourceStore = [coordinator persistentStoreForURL:oldStoreURL];
if (sourceStore != nil){
// Perform the migration
destinationStore = [coordinator migratePersistentStore:sourceStore toURL:storeURL options:storeOptions withType:NSSQLiteStoreType error:&error];
if (destinationStore == nil){
// Handle the migration error
} else {
// You can now remove the old data at oldStoreURL
// Note that you should do this using the NSFileCoordinator/NSFilePresenter APIs, and you should remove the other files
// described in QA1809 as well.
}
}
}

Once the migration has completed you can delete the old files. The example here explicitly specifies the SQLite journal options, this is to ensure that if the default options are changed in the future the code will still work. If you are using different options, you should use those instead.

How to re-use Core Data in Extension code in Objective-C? The managedObjectContext can't be created by UIApplicationDelegate as we use to do

We found and fix the problem now. The problem is caused by my misunderstanding of Core Data. We used to re-use tutorial's source code. When the system is work, we have no time to get deep understand of it.

The problem is we can't use Container App's managed object context. To fix it we use following code:
replace

[((MDAppDelegate*)appController) mainQueueContext] ;

to

[self mainQueueContext] ;

Then, add following...

- (NSManagedObjectContext *)mainQueueContext {
if (_mainQueueContext != nil) {
return _mainQueueContext;
}
_mainQueueContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainQueueContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[_mainQueueContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
return _mainQueueContext; }

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSURL *storeURL;
NSString *containerPath = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.<your project group ID>"].path;
NSString *sqlitePath = [NSString stringWithFormat:@"file://%@/%@", containerPath, @"<Your database file>.sqlite"];
storeURL = [NSURL URLWithString:sqlitePath];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSDictionary *options = @{
NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES
};
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeURL
options:options
error:&error]) {
abort();
}
NSDictionary *fileAttributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
if (![[NSFileManager defaultManager] setAttributes:fileAttributes ofItemAtPath:storeURL.path error:&error]) {
// Handle error
}
return _persistentStoreCoordinator;
}

- (NSManagedObjectModel *)managedObjectModel
{
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL;
NSString *containerPath = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.<your project group ID>"].path;
NSString *modelPath = [NSString stringWithFormat:@"file://%@/%@", containerPath, @"<Your database file>.momd"];
modelURL = [NSURL URLWithString:modelPath];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}

Another Important issue is the Container App is still using the Core Data store located in application's Documents directory. The extension can't access it. So, we migrate the store to group shared folder using below lines:

    NSString *directoryShared = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.<your project group ID>"].path;
NSString *storePathShared = [NSString stringWithFormat:@"file://%@/%@", directoryShared, @"<Your database file>.sqlite"];
NSURL *storeUrlShared = [NSURL URLWithString:storePathShared];
[_persistentStoreCoordinator migratePersistentStore:store
toURL:storeUrlShared
options:options
withType:NSSQLiteStoreType
error:&error];
if (error != nil) {
NSLog(@"Error when migration to groupd url %@, %@", error, [error userInfo]);
}

We just leave the original database alone, let the container app keep using it. there are some better way to achieve it. like:
Migrating NSPersistentStore from application sandbox to shared group container



Related Topics



Leave a reply



Submit