代码之家  ›  专栏  ›  技术社区  ›  hpique

打开/关闭iCloud时迁移数据

  •  12
  • hpique  · 技术社区  · 11 年前

    本地帐户

    WWDC 2013 207 session 关于核心数据和iCloud:

    您在应用程序的本地内部为我们提供了一个单一的存储URL 沙盒,然后我们创建一个不透明的容器,其中包含 它适用于系统上的每个帐户,包括本地帐户 是我们的术语,用于描述在 系统这是一个由Core Data管理的特殊商店 你不必做任何特殊的事情,因为你的用户没有 iCloud帐户。

    在iOS 7/OS X 10.9中,iCloud的核心数据将自动使用 本地帐户 适用于iCloud关闭的情况。与 后备存储器 (当iCloud打开但无法访问时使用) 本地帐户将完全由iCloud帐户取代 当服务打开时,不进行任何合并。只有当iCloud关闭时,才能访问本地帐户中的数据。这种情况发生在:

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

    以上是我从实验中学到的。如果我错了,请纠正我。

    数据消失时

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

    我见过一些例子,试图通过向应用程序添加(更多)iCloud设置并管理自己的“本地”商店(而不是iCloud提供的商店)来解决这个问题。这对我来说有复制工作的味道。

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

    这种方法怎么样?

    • 无论iCloud是否打开,始终使用核心数据和iCloud。
    • 当iCloud从关闭变为打开时,询问用户是否要将本地帐户与iCloud帐户合并 。如果是,请合并、删除重复项,并按本地优先级排序并清空本地帐户。
    • 当iCloud从打开变为关闭时,询问用户是否要将iCloud商店与本地帐户合并 。如果是,请合并并删除重复的iCloud优先级。

    这与 提醒 做然而 提醒 直接从iCloud设置向用户询问数据迁移,这是我们开发人员无法做到的。

    问题

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

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

    3) 您是否会执行用户提示并在 NSPersistenStoreCoordinatorStoresWillChangeNotification NSPersistentStoreCoordinatorStoresDidChangeNotification ,或者收集其中的所有信息并等待商店更改?我这样问是因为这些通知似乎是在后台发送的,阻止它们执行可能较长的操作可能不是核心数据所期望的。

    2 回复  |  直到 11 年前
        1
  •  6
  •   Duncan Groenewald    11 年前

    我认为你误解了207届会议上所说的话。

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

    因为新应用程序的默认安全设置Data&首次安装应用程序时,文档处于打开状态。您必须询问用户是否要使用iCloud。试试苹果的Pages应用程序。

    如果用户随后更改了首选项设置,则应用程序必须将存储迁移到iCloud或从iCloud迁移。

    核心数据自动处理的部分是,如果您切换iCloud帐户(注销并使用其他帐户登录),则应用程序将使用登录到此帐户时可能创建的任何核心数据存储运行。

    请参阅下面的文字记录,其中非常清楚地指出,当帐户注销时,iCloud商店将被删除。它不见了,木棉花,一只死鹦鹉。因此,当您有机会保存更改日志时,只有更改日志保留在本地,以防将来再次使用该帐户。

    您只需实现您的意愿更改处理程序并响应 NSPersistentStoreCoordinator门店将更改并通知您 因为 系统上有新帐户。

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

    现在,一旦你做到了这一点,我们将从协调器中删除商店 就像异步设置过程一样,然后我们将向您发送 NSPersistentStoreCoordinator存储已更改通知, 就像异步设置一样,您可以开始使用 应用程序。

    现在,让我们更详细地讨论一下这个问题。

    收到NSPersistentStoreCoordinator存储将更改 通知,持久存储仍然可以使用,因此 不像我们去年告诉你的那样,你必须立即 删除持久存储并清除托管对象上下文, 仍然可以写入托管对象上下文 以及这些变化 将在本地持久化,以便导入到帐户 回来了 .

    这意味着,尽管用户的更改不会影响iCloud 立即,如果他们再次登录,他们会在那里等待 .

    最后,所有这些存储文件将由Core Data和 这意味着我们可以随时删除它们。

    每个商店的帐户注销后都将被删除 因为我们 可以从云中重建文件。

    因此,我们希望为您的 要使用的应用程序,而不是旧的存储文件 可能会占用额外的资源。

    再往前走一点

    我们也是 引入新选项以帮助您创建备份或 iCloud永久存储的本地副本 称为NSPersistentStore 删除无处不在的元数据选项。

    这将从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的核心数据应用程序
    • 使用 地方的 苹果云 核心数据存储
    • 包括 设置捆绑包 (请注意,这将在设置应用程序中创建一个设置页面),其中包括:
      • 使用iCloud 首选项设置(ON或OFF)
      • 制作备份 首选项设置(ON或OFF)
      • 显示应用程序 版本 内部版本号
    • 提示用户有关存储选项 使用iCloud 首选项更改为ON
    • 将核心数据存储迁移到iCloud 取决于用户的首选项设置和对提示的响应
    • 检测从其他设备删除iCloud存储 并通过创建新的空iCloud商店进行清理
    • 迁移时检查现有iCloud文件 本地存储到iCloud,并提示用户如果存在iCloud文件,是否合并或放弃本地存储中的数据
    • 进行备份 如果 制作备份 首选项设置为ON。备份文件名为 persistentStore_Backup_yyyy_MM_dd_HH_MM_ss。 要使用它:
      • 将“备份”首选项设置为“打开”,下次激活应用程序时,它将备份当前核心数据存储并将首选项重置为“关闭”
      • 文件可以从iTunes复制到PC或Mac
      • 要还原,只需将应用程序设置为使用本地文件(使用iCloud首选项关闭),并用所需的备份文件替换persistentStore文件(注意,必须调用该文件 持久性存储 ).
    • 编辑记录并保存/取消编辑 在详细视图中
    • 异步打开核心数据存储 确保长时间迁移不会阻塞主线程并导致App终止
    • 在后台线程上加载数据 具有 在主UITableView中拉到“刷新”以启动另一个后台线程(您可以同时启动多个后台线程,请注意!)
    • 在detailView中显示相关对象 使用UITableView、fetchedResultsController和谓词筛选选择
    • 如果没有存储,则加载种子数据 ,检查iCloud文件是否已由其他设备创建
    • iCloud上载/下载状态指示器 ,当核心数据事务日志需要同步、正忙于同步、正在导入或正在运行后台任务时,网络活动指示器将打开
    • 提要栏样式UI 具有iOS和OS X应用程序的多个主视图和详细视图
    • 备份文件管理器 它允许您进行备份、将备份文件复制到iCloud和从iCloud复制备份文件、通过电子邮件发送和接收备份文件以及从备份文件恢复。
        2
  •  3
  •   hpique    11 年前

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

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

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

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

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

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

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

    2) NSPersistenStoreCoordinatorStoresWillChangeNotification和 NSPersistentStoreCoordinatorStoresDidChangeNotification足以 检测所有可能的从开到关和从关到开的iCloud转换?

    对它要求您始终使用 NSPersistentStoreUbiquitousContentNameKey ,无论iCloud是否打开或关闭。如下所示:

    [self.managedObjectContext.persistentStoreCoordinator
     addPersistentStoreWithType:NSSQLiteStoreType
     configuration:nil
     URL:storeURL
     options:@{ NSPersistentStoreUbiquitousContentNameKey : @"someName" }
     error:&error];
    

    事实上,只听 NSPersistentStoreCoordinatorStoresDidChangeNotification 足够了(如下所示)。当在启动时添加存储或在执行期间更改存储时,将调用此命令。

    3) 您是否会执行用户提示并在 NSPersistenStoreCoordinatorStoresWillChangeNotification和 NSPersistentStoreCoordinatorStoresDidChangeNotification,或收集所有 然后等待商店更改?我问 因为这些通知似乎是在后台发送的,并且 阻止它们执行可能较长的操作可能不是 核心数据所期望的。

    我会这样做 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
        });
    }
    

    然后:

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

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

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