打开/关闭iCloud时迁移数据

hpi*_*que 9 macos core-data core-data-migration ios icloud

本地帐户

关于Core Data和iCloud 的WWDC 2013 207会议:

您在应用程序的本地沙箱中为我们提供单个商店URL,然后我们为系统中的每个帐户创建一个不透明容器,其中包含一个条目,包括本地帐户,这是我们在没有iCloud帐户时会发生什么的术语在系统上.这是一个由Core Data管理的特殊商店,因此您无需执行任何特殊操作,因为您的用户没有iCloud帐户.

在iOS 7/OS X 10.9中,带有iCloud的Core Data将自动使用本地帐户来处理iCloud关闭的情况.与后备存储(在iCloud打开但无法访问时使用)不同,本地帐户将在服务启用时由iCloud帐户全部替换,而不进行任何合并.只有iCloud关闭时,才能访问本地帐户中的数据.这种情况发生在:

  • 没有iCloud帐户.
  • 有一个iCloud帐户,但"文档和数据"已被禁用.
  • 有一个iCloud帐户,但该应用程序已在"文档和数据"中禁用.

以上是我从实验中理解的.如果我错了,请纠正我.

数据消失时

按原样使用,本地帐户用户体验非常糟糕.如果您在关闭iCloud的情况下向应用添加数据然后将其打开,数据将"消失",您可能会认为它已被删除.如果您在启用了iCloud的应用程序中添加数据,然后将其关闭,则数据也将"消失".

我已经看到了一些例子,试图通过向应用添加(更多)iCloud设置并管理他们自己的"本地"商店(不是iCloud提供的商店)来解决这个问题.这让我重复工作.

利用本地帐户进行数据迁移

这种方法怎么样?

  • 无论iCloud是打开还是关闭,始终使用Core Data和iCloud.
  • 当iCloud从off变为on时,询问用户是否要将本地帐户与iCloud帐户合并.如果是,请合并,删除重复项,优先考虑本地帐户并清空本地帐户.
  • 当iCloud从开启到关闭时,询问用户是否要将iCloud商店与本地帐户合并.如果是,请合并并删除重复项,以确定iCloud的优先级.

这类似于Reminders所做的.但是,Reminders直接从iCloud设置向用户询问数据迁移,这是我们开发人员无法做到的.

问题

1)这种方法是否有任何缺点或边界情况乍一看可能不明显?也许我们不打算像这样使用iCloud生成的本地帐户.

2)是否NSPersistentStoreCoordinatorStoresWillChangeNotificationNSPersistentStoreCoordinatorStoresDidChangeNotification足以检测出所有可能的上断和断到在iCloud上的转变?

3)你会做的用户提示和合并之间NSPersistentStoreCoordinatorStoresWillChangeNotificationNSPersistentStoreCoordinatorStoresDidChangeNotification,或聚集在这些所有的信息和等待商店改变了吗?我问,因为这些通知似乎是在后台发送的,阻止它们执行可能很长的操作可能不是Core Data所期望的.

Dun*_*ald 6

我想你误解了207会议上的内容.

Core Data不会自动为您创建本地和iCloud存储,也不会在iCloud帐户关闭时同步数据.根据用户选择的内容,您必须使用NSPersistentStoreUbiquityNameKey选项(对于iCloud存储)或不使用它(对于本地存储)来创建存储.

由于首次安装应用程序时新应用程序数据和文档的默认安全设置为ON,因此您必须询问用户是否要使用iCloud.尝试使用Apple的Pages应用程序.

如果用户随后更改了首选项设置,则您的应用必须将商店迁移到iCloud或从iCloud迁移.

Core Data自动处理的部分是,如果您切换iCloud帐户(注销并使用其他帐户登录),则App将在登录此帐户时运行可能已创建的任何Core Data存储.

请参阅下面的脚本,其中明确指出当帐户消失时iCloud商店将被删除.它消失了,卡普特,一只死鹦鹉.因此,当您有机会保存时,只有更改日志保留在本地,以防将来再次使用该帐户.

您只需实现您的意愿更改处理程序并响应NSPersistentStoreCoordinator Stores Will Change,并在我们需要更改持久存储文件时自动通知您,因为系统上有新帐户.

当然,您可以调用NSManagedObjectContext save和NSManagedObjectContext重置.

现在,一旦你完成了,我们将从协调器中删除存储,就像异步设置过程一样,然后我们将再次发送NSPersistentStoreCoordinator Storage Did Change通知,就像异步设置一样,你可以开始使用你的像往常一样申请.

现在,让我们更详细地谈谈这个问题.

当您收到NSPersistentStoreCoordinator Stores Will Change通知时,持久存储仍可供使用,因此与我们去年建议您不得不立即删除持久存储并清除托管对象上下文的情况不同,您仍然可以写入托管对象上下文和这些更改将在本地持久保存,以便在每次更改时都导入到该帐户.

这意味着虽然您的用户的更改不会立即进入iCloud,但如果他们再次登录,他们将会在那里等待.

最后,所有这些商店文件都将由Core Data管理,这意味着我们可以随时删除它们.

一旦其帐户消失,每个商店都将被删除,因为我们可以从云重建文件.

因此,我们希望释放尽可能多的磁盘空间供您的应用程序使用,而不是存在可能占用额外资源的旧存储文件.

还有一点

我们还引入了一个新选项来帮助您创建名为NSPersistentStore 的iCloud持久存储的备份或本地副本删除无处不在的元数据选项.

这将从iCloud存储中删除所有关联的元数据; 这意味着,我们写入元数据字典以及商店文件本身的任何内容,如果您希望使用迁移API在您希望在没有iCloud选项的情况下打开的持久性商店中创建备份或本地副本,这一点至关重要.

另外,请看一下Tim Roadley的书的勘误链接

http://timroadley.com/2014/02/13/learning-core-data-for-ios-errata/

如果您登录到iCloud然后用户更改了应用程序首选项设置(与数据和文档安全设置不同)以关闭您的应用程序iCloud,则应询问用户是否要将现有iCloud存储迁移到本地(再次 - 尝试使用Pages,看看你得到了什么消息).

我发布了一个示例应用程序,在这里完成所有这些.看一下视频,看看预期的行为. http://ossh.com.au/design-and-technology/software-development/

示例应用程序的一些功能包括:

功能包括:

  • 使用iCloud Integration的iOSOSX核心数据应用示例
  • 使用本地iCloud核心数据存储
  • 包括设置包(请注意,这会在"设置"应用程序中创建一个设置页面),其中包括:
    • 使用iCloud首选项设置(开或关)
    • 进行备份首选项设置(ON或OFF)
    • 显示应用程序版本内部版本
  • Use iCloud首选项更改为ON时,提示用户有关存储选项的信息
  • 根据用户首选项设置和对提示的响应,将Core Data存储迁移到iCloud或从iCloud迁移
  • 检测从其他设备删除iCloud存储,并通过创建新的空iCloud存储来清理
  • 本地存储迁移到iCloud时检查现有的iCloud文件,如果存在iCloud文件,则提示用户是否合并或丢弃本地存储中的数据
  • 使得备份核心数据存储的,如果做备份首选项设置为ON.备份文件名为 persistentStore_Backup_yyyy_MM_dd_HH_mm_ss.要使用它:
    • 将备份首选项设置为ON,下次启动应用程序时,它将备份当前的Core Data存储并将首选项重置为OFF
    • 文件可以从iTunes复制到PC或Mac
    • 恢复简单设置app以使用本地文件(使用iCloud首选项OFF)并将persistentStore文件替换为所需的备份文件(注意该文件必须称为persistentStore).
  • 在详细视图中编辑记录并保存/取消编辑
  • 异步打开Core Data存储以确保长时间迁移不会阻止主线程并导致App终止
  • 在主UITableView中使用Pull to Refresh 加载后台线程上的数据以启动另一个后台线程(您可以启动多个后台线程同时运行,注意!) 
  • 使用UITableView,fetchedResultsController和谓词过滤选择,在detailView中显示相关对象
  • 如果已存在no store ,则加载种子数据,检查是否已由其他设备创建iCloud文件
  • iCloud上传/下载状态指示灯,当需要同步Core Data事务日志,忙于同步,导入或后台任务正在运行时,网络活动指示灯亮起
  • 侧栏样式UI ,包含iOS和OS X应用程序的多个主视图和详细视图
  • 备份文件管理器 ,允许您进行备份,将备份文件复制到iCloud或从iCloud复制备份文件,通过电子邮件发送和接收备份文件以及从备份文件还原.


hpi*_*que 3

我将尝试回答我自己的问题,部分是为了整理我的想法,部分是为了回复@DuncanGroenewald。

1)这种方法是否有任何乍一看并不明显的缺点或边界情况?

是的。本地和 iCloud 帐户存储由 Core Data 管理,可以随时删除。

实际上,我认为本地帐户存储不会被删除,因为它无法从 iCloud 重新创建。

关于 iCloud 帐户存储,我可以看到两种可能被删除的情况:a) 在用户关闭 iCloud 后释放空间,或 b) 因为用户通过选择“设置”>“iCloud”>“全部删除”来请求它。

如果用户请求它,那么您可能会认为数据迁移不是问题。

如果是为了释放空间,那么是的,这是一个问题。然而,任何其他方法都存在同样的问题,因为当 iCloud 帐户存储被删除时,您的应用程序不会被唤醒。

2) NSPersistentStoreCoordinatorStoresWillChangeNotification 和 NSPersistentStoreCoordinatorStoresDidChangeNotification 是否足以检测所有可能的打开到关闭以及关闭到打开 iCloud 转换?

是的。它要求您始终使用 创建持久性存储NSPersistentStoreUbiquitousContentNameKey,无论 iCloud 是否打开。像这样:

[self.managedObjectContext.persistentStoreCoordinator
 addPersistentStoreWithType:NSSQLiteStoreType
 configuration:nil
 URL:storeURL
 options:@{ NSPersistentStoreUbiquitousContentNameKey : @"someName" }
 error:&error];
Run Code Online (Sandbox Code Playgroud)

其实只听NSPersistentStoreCoordinatorStoresDidChangeNotification就够了(如下图)。当在启动时添加存储或在执行期间更改存储时,将调用此函数。

3)您会在 NSPersistentStoreCoordinatorStoresWillChangeNotification 和 NSPersistentStoreCoordinatorStoresDidChangeNotification 之间进行用户提示和合并,还是收集其中的所有信息并等待存储更改?我之所以这么问,是因为这些通知似乎是在后台发送的,并且阻止它们执行可能较长的操作可能不是 Core Data 所期望的。

这就是我在 中的做法NSPersistentStoreCoordinatorStoresDidChangeNotification

由于此通知会在启动时以及执行过程中商店发生更改时发送,因此我们可以使用它来保存当前商店 url 和普遍存在的身份令牌(如果有)。

然后我们检查是否处于开/关转换场景并相应地迁移数据。

为了简洁起见,我没有包含任何 UI 代码、用户提示或错误处理。在进行任何迁移之前,您应该询问(或至少通知)用户。

- (void)storesDidChange:(NSNotification *)notification
{
    NSDictionary *userInfo = notification.userInfo;
    NSPersistentStoreUbiquitousTransitionType transitionType = [[userInfo objectForKey:NSPersistentStoreUbiquitousTransitionTypeKey] integerValue];
    NSPersistentStore *persistentStore = [userInfo[NSAddedPersistentStoresKey] firstObject];
    id<NSCoding> ubiquityIdentityToken = [NSFileManager defaultManager].ubiquityIdentityToken;

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    if (transitionType != NSPersistentStoreUbiquitousTransitionTypeInitialImportCompleted) { // We only care of cases if the store was added or removed
        NSData *previousArchivedUbiquityIdentityToken = [defaults objectForKey:HPDefaultsUbiquityIdentityTokenKey];
        if (previousArchivedUbiquityIdentityToken) { // Was using ubiquity store
            if (!ubiquityIdentityToken) { // Changed to local account
                NSString *urlString = [defaults objectForKey:HPDefaultsPersistentStoreURLKey];
                NSURL *previousPersistentStoreURL = [NSURL URLWithString:urlString];
                [self importPersistentStoreAtURL:previousPersistentStoreURL
                 isLocal:NO
                 intoPersistentStore:persistentStore];
            }
        } else { // Was using local account
            if (ubiquityIdentityToken) { // Changed to ubiquity store
                NSString *urlString = [defaults objectForKey:HPDefaultsPersistentStoreURLKey];
                NSURL *previousPersistentStoreURL = [NSURL URLWithString:urlString];
                [self importPersistentStoreAtURL:previousPersistentStoreURL 
                 isLocal:YES 
                 intoPersistentStore:persistentStore];
            }
        }
    }
    if (ubiquityIdentityToken) {
        NSData *archivedUbiquityIdentityToken = [NSKeyedArchiver archivedDataWithRootObject:ubiquityIdentityToken];
        [defaults setObject:archivedUbiquityIdentityToken forKey:HPModelManagerUbiquityIdentityTokenKey];
    } else {
        [defaults removeObjectForKey:HPModelManagerUbiquityIdentityTokenKey];
    }
    NSString *urlString = persistentStore.URL.absoluteString;
    [defaults setObject:urlString forKey:HPDefaultsPersistentStoreURLKey];
    dispatch_async(dispatch_get_main_queue(), ^{
        // Update UI
    });
}
Run Code Online (Sandbox Code Playgroud)

然后:

- (void)importPersistentStoreAtURL:(NSURL*)importPersistentStoreURL 
    isLocal:(BOOL)isLocal 
    intoPersistentStore:(NSPersistentStore*)persistentStore 
{
    if (!isLocal) { 
        // Create a copy because we can't add an ubiquity store as a local store without removing the ubiquitous metadata first,
        // and we don't want to modify the original ubiquity store.
        importPersistentStoreURL = [self copyPersistentStoreAtURL:importPersistentStoreURL];
    }
    if (!importPersistentStoreURL) return;

    // You might want to use a different concurrency type, depending on how you handle migration and the concurrency type of your current context
    NSManagedObjectContext *importContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
    importContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
    NSPersistentStore *importStore = [importContext.persistentStoreCoordinator
            addPersistentStoreWithType:NSSQLiteStoreType
            configuration:nil
            URL:importPersistentStoreURL
            options:@{NSPersistentStoreRemoveUbiquitousMetadataOption : @(YES)}
            error:nil];

    [self importContext:importContext intoContext:_managedObjectContext];
    if (!isLocal) {
        [[NSFileManager defaultManager] removeItemAtURL:importPersistentStoreURL error:nil];
    }
}
Run Code Online (Sandbox Code Playgroud)

数据迁移是在importContext:intoContext. 此逻辑将取决于您的模型以及重复和冲突策略。

我不知道这是否会产生不需要的副作用。显然,这可能需要相当长的时间,具体取决于持久存储的大小和数据。如果我发现任何问题,我会编辑答案。