coredata - move to app group target

2020-02-09 10:09发布

I am new to the Today extension and using an embedded framework.

Our app currently uses core data backed by sqlite. If I want to share this between the app and the today extension, should I move this to a framework to be shared by both?

How can I migrate our current version in the app store to be able to upgrade to a new structure?

4条回答
时光不老,我们不散
2楼-- · 2020-02-09 10:29

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.

查看更多
迷人小祖宗
3楼-- · 2020-02-09 10:36

For the migration of old data part, I've done some work.

  1. how to check old db exist or not, I use below code to check.

    if ([fileManager fileExistsAtPath:[storeURL path]]) {
    NSLog(@"old single app db exist.");
    targetURL = storeURL;
    needMigrate = true;
    }
    // storeURL is the store url return by:
    - (NSURL *)applicationDocumentsDirectory
    {
       return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
    }
    
  2. Migrate old data to new data store place.

If the old data store exist, and the group data store not exist, I use below code do the migrate:

if (needMigrate) {
    NSError *error = nil;
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [context setPersistentStoreCoordinator:__persistentStoreCoordinator];
    [__persistentStoreCoordinator migratePersistentStore:store toURL:groupURL options:options withType:NSSQLiteStoreType error:&error];
    if (error != nil) {
        NSLog(@"Error when migration to groupd url %@, %@", error, [error userInfo]);
        abort();
    }
}

the needMigrate flag is set by checking if old data exist.

  1. if no data exist, I will use group url to crate the PSC.

For your reference, I paste full code here:

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (__persistentStoreCoordinator != nil)
    {
       return __persistentStoreCoordinator;
    }

bool needMigrate = false;
bool needDeleteOld  = false;

NSString *kDbName = @"xxx.sqlite";

NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:kDbName];

NSURL *groupURL = [[self applicationGroupDocumentDirectory] URLByAppendingPathComponent:kDbName];

NSURL *targetURL =  nil;

NSFileManager *fileManager = [NSFileManager defaultManager];

if ([fileManager fileExistsAtPath:[storeURL path]]) {
    NSLog(@"old single app db exist.");
    targetURL = storeURL;
    needMigrate = true;
}


if ([fileManager fileExistsAtPath:[groupURL path]]) {
       NSLog(@"group db exist");
       needMigrate = false;
       targetURL = groupURL;

    if ([fileManager fileExistsAtPath:[storeURL path]]) {
        needDeleteOld = true;
    }
}

if (targetURL == nil)
    targetURL = groupURL;

NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption: @(YES),
                            NSInferMappingModelAutomaticallyOption: @(YES)};

NSError *error = nil;
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];


NSPersistentStore *store;
store = [__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:targetURL options:options error:&error];

if (!store)
{
    /*
     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 persistent store is not accessible;
     * The schema for the persistent store is incompatible with current managed object model.
     Check the error message to determine what the actual problem was.


     If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.

     If you encounter schema incompatibility errors during development, you can reduce their frequency by:
     * Simply deleting the existing store:
     [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]

     * Performing automatic lightweight migration by passing the following dictionary as the options parameter:
     [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption, [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

     Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.

     */
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

// do the migrate job from local store to a group store.
if (needMigrate) {
    NSError *error = nil;
    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [context setPersistentStoreCoordinator:__persistentStoreCoordinator];
    [__persistentStoreCoordinator migratePersistentStore:store toURL:groupURL options:options withType:NSSQLiteStoreType error:&error];
    if (error != nil) {
        NSLog(@"Error when migration to groupd url %@, %@", error, [error userInfo]);
        abort();
    }
}

return __persistentStoreCoordinator;
}

/**
 Returns the URL to the application's Documents directory.
 */
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}

- (NSURL *)applicationGroupDocumentDirectory
{
return [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.com.kzjeef.shitf.scheduler"];
}
查看更多
够拽才男人
4楼-- · 2020-02-09 10:38

For Migration in swift 3.0 and above just replace below persistentStoreCoordinator method with yours

lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator? = {

    var coordinator: NSPersistentStoreCoordinator? = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
    let options = [
        NSMigratePersistentStoresAutomaticallyOption: true,
        NSInferMappingModelAutomaticallyOption: true
    ]

    let oldStoreUrl = self.applicationDocumentsDirectory.appendingPathComponent("TaskTowerStorage.sqlite")
    let directory: NSURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: AppGroupID)! as NSURL
    let newStoreUrl = directory.appendingPathComponent("YourDatabaseName.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: options)
            if let store = coordinator?.persistentStore(for: targetUrl!)
            {
                do {
                    try coordinator?.migratePersistentStore(store, to: newStoreUrl, options: options, withType: NSSQLiteStoreType)

                } catch let error {
                    print("migrate failed with error : \(error)")
                }
            }
        } catch let error {
            //CrashlyticsHelper.reportCrash(err: error as NSError, strMethodName: "migrateStore")
            print(error)
        }
    }

    if needDeleteOld {
        self.deleteDocumentAtUrl(url: oldStoreUrl)
        self.deleteDocumentAtUrl(url: self.applicationDocumentsDirectory.appendingPathComponent("YourDatabaseName.sqlite-shm"))
        self.deleteDocumentAtUrl(url: self.applicationDocumentsDirectory.appendingPathComponent("YourDatabaseName.sqlite-wal"))
    }

    do {
        try coordinator!.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: targetUrl, options: options)
    } catch var error as NSError {
        coordinator = nil
        NSLog("Unresolved error \(error), \(error.userInfo)")
        abort()
    } catch {
        fatalError()
    }
    return coordinator

}() 
     lazy var applicationDocumentsDirectory: URL = {
// The directory the application uses to store the Core Data store file. This code uses a directory named 'Bundle identifier' in the application's documents Application Support directory.
    let urls = Foundation.FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return urls[urls.count-1]
}()


lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = Bundle.main.url(forResource: "StorageName", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
查看更多
Luminary・发光体
5楼-- · 2020-02-09 10:43

You need to make sure both the model and the persistent store file are available to both the app and the extension.

For the model, moving it to a framework is a good idea since it means there's only one copy of the model file. As long as both the app and the extension link to the framework, it'll be available to both. If you do that, it's probably a good idea to put the code that sets up the Core Data stack in the framework too, since it'll be the same in both cases.

You can of course just include the model in both targets. That will mean you'll have two copies of the file, which wastes space. Probably not a lot of space, though.

For the persistent store, you'll have to set up an app group and use a store file in the group directory. App groups is one of the settings in "capabilities" for the app and extension-- turn it on and create a group name. Then put the persistent store file in the group directory, which you can find using code like

NSURL *groupURL = [[NSFileManager defaultManager]
    containerURLForSecurityApplicationGroupIdentifier:
        @"GROUP NAME HERE"];

[I cover some of this in more detail at my blog].

If you have existing data, you'll have to move it to the new store file. This would look something like

  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.
查看更多
登录 后发表回答