iOS 11+ Как перенести существующие базовые данные в Shared App Group для использования в расширении?

Когда я создал приложение iOS 11 с использованием шаблона основных данных, оно автоматически сгенерировало следующий код в AppDelete.m.

synthesize persistentContainer = _persistentContainer;

- (NSPersistentContainer *)persistentContainer {
    // The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
    @synchronized (self) {
        if (_persistentContainer == nil) {
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"My_History"];
            [_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
                if (error != nil) {
                    // 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 _persistentContainer;
}

- (void)saveContext {
NSManagedObjectContext *context = self.persistentContainer.viewContext;
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
    // 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.
    NSLog(@"Unresolved error %@, %@", error, error.userInfo);
    abort();
}

Я хотел бы добавить расширение Today и iMessage, которое обращается к истории в основных данных. Из того, что я прочитал, мне нужно перенести эти данные, если они существуют, в общий контейнер приложения. Как бы я это сделал?

Код в цели C.

Я читал другие вопросы, связанные с этим, но все они, кажется, были до того, как Apple изменила способ работы с основными данными, чтобы упростить его. Как вы можете видеть в моем коде, я никогда не указывал точное имя файла хранилища данных. Каждый пример, который я видел, имел что-то вроде "My_History.sqllite". Я даже не знаю, является ли моя база данных sql lite, она была просто создана этим кодом.

4 ответа

Решение

Я закончил тем, что получил это, делая следующее. Файл sqlite на самом деле был названием моего init плюс.sqlite в конце.

+ (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;
}

SolidSnake4444 спасает мой день. Вот версия Swift 5.0.

lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "MyApp")
    let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.my.app")!.appendingPathComponent("MyApp.sqlite")

    var defaultURL: URL?
    if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
        defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
    }

    if defaultURL == nil {
        container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
    }
    container.loadPersistentStores(completionHandler: { [unowned container] (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }

        if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
            let coordinator = container.persistentStoreCoordinator
            if let oldStore = coordinator.persistentStore(for: url) {
                do {
                    try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
                } catch {
                    print(error.localizedDescription)
                }

                // delete old store
                let fileCoordinator = NSFileCoordinator(filePresenter: nil)
                fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
                    do {
                        try FileManager.default.removeItem(at: url)
                    } catch {
                        print(error.localizedDescription)
                    }
                })
            }
        }
    })
    return container
}()

При движении NSPersistentCloudKitContainer, это то, что сработало для меня. Пара вещей, на которые следует обратить внимание:

  • Вам необходимо убедиться cloudKitContainerOptionsустанавливается в описании постоянного хранилища, иначе синхронизация перестанет работать
  • Не звони migratePersistentStoreв противном случае вы получите повторяющиеся записи, вместо этого используйте replacePersistentStore- см. эту тему для получения дополнительной информации
  • Вы, вероятно, захотите удалить свою старую базу данных после успешной миграции, destroyPersistentStoreзвучит многообещающе, но, похоже, на самом деле не удаляет файлы - см. этот вопрос
      struct PersistenceController {
    static let shared = PersistenceController()

    let container: NSPersistentCloudKitContainer

    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "MyCuteDB")
        
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        } else {
            // Use App Groups so app and extensions can access database
            let sharedStoreURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourdomain.yourapp")!.appendingPathComponent("\(container.name).sqlite")
            let defaultStoreURL = container.persistentStoreDescriptions.first!.url! //the URL always starts out in the default location

            // move database to shared location if needed
            if FileManager.default.fileExists(atPath: defaultStoreURL.path) && !FileManager.default.fileExists(atPath: sharedStoreURL.path) {
                let coordinator = container.persistentStoreCoordinator
                do {
                    try coordinator.replacePersistentStore(at: sharedStoreURL, destinationOptions: nil, withPersistentStoreFrom: defaultStoreURL, sourceOptions: nil, ofType: NSSQLiteStoreType)
                    try? coordinator.destroyPersistentStore(at: defaultStoreURL, ofType: NSSQLiteStoreType, options: nil)
                    
                    // destroyPersistentStore says it deletes the old store but seems to be a lie so we'll manually delete the files
                    NSFileCoordinator(filePresenter: nil).coordinate(writingItemAt: defaultStoreURL.deletingLastPathComponent(), options: .forDeleting, error: nil, byAccessor: { url in
                        try? FileManager.default.removeItem(at: defaultStoreURL)
                        try? FileManager.default.removeItem(at: defaultStoreURL.deletingLastPathComponent().appendingPathComponent("\(container.name).sqlite-shm"))
                        try? FileManager.default.removeItem(at: defaultStoreURL.deletingLastPathComponent().appendingPathComponent("\(container.name).sqlite-wal"))
                        try? FileManager.default.removeItem(at: defaultStoreURL.deletingLastPathComponent().appendingPathComponent("ckAssetFiles"))
                    })
                } catch {
                    //TODO: Handle error
                }
            }

            // change URL from default to shared location
            if FileManager.default.fileExists(atPath: sharedStoreURL.path) {
                container.persistentStoreDescriptions.first!.url = sharedStoreURL
            }
        }
            
        let description = container.persistentStoreDescriptions.first!
        description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.yourdomain.yourapp")
        
        container.loadPersistentStores { description, error in
            //TODO: Handle error
        }
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.automaticallyMergesChangesFromParent = true
    }
}

ОБНОВИТЬ:

Для переноса существующего постоянного хранилища NSPersistentContainer содержит persistentStoreCoordinator, экземпляр NSPersistentContainerCoordinator, Это подвергает метод migratePersistentStore:toURL:options:withType:error: перенести постоянный магазин.

Я бы сделал следующее:

// Get the reference to the persistent store coordinator
let coordinator = persistentContainer.persistentStoreCoordinator
// Get the URL of the persistent store
let oldURL = persistentContainer.persistentStoreDescriptions.url
// Get the URL of the new App Group location
let newURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("YOUR_APP_GROUP")
// Get the reference to the current persistent store
let oldStore = coordinator.persistentStore(for: oldURL)
// Migrate the persistent store
do {
   try coordinator.migratePersistentStore(oldStore, to: newURL, options: nil, withType: NSSQLiteStoreType)
} catch {
   // ERROR
}

Обратите внимание, что вышеупомянутое не было проверено, и я не обработал Дополнительные, поэтому это не завершено. Кроме того, я прошу прощения за это в Свифте. Надеюсь, вам достаточно легко написать эквивалент в Objective-C.

ОРИГИНАЛ:

Ниже описано, как создать NSPersistentContainer взаимодействие с постоянным хранилищем в нестандартном местоположении.

NSPersistentContainer подвергает defaultDirectoryURL и утверждает:

Этот метод возвращает платформо-зависимый NSURL в котором постоянные хранилища будут расположены или в настоящее время находятся. Этот метод может быть переопределен в подклассе NSPersistentContainer,

Если вы подкласс NSPersistentContainer и определить defaultDirectoryURL быть каталогом группы приложений, используя containerURLForSecurityApplicationGroupIdentifier После этого вы сможете получить доступ к контейнеру между вашим приложением и расширениями (при условии, что они имеют одинаковые права доступа группы приложений).

NSPersistentContainer также выставляет persistentStoreDescriptions который также имеет экземпляр URL. Кроме того, вы можете обновить его до URL группы приложений перед вызовом loadPersistentStoresWithCompletionHandler:,

Обратите внимание, что я не использовал NSPersistentContainer и не знаю, вызовет ли это совместное использование какие-либо проблемы с параллелизмом.

Другие вопросы по тегам