【问题标题】:Migrating data when iCloud is turned on/off打开/关闭 iCloud 时迁移数据
【发布时间】:2014-03-20 15:50:57
【问题描述】:

本地帐号

来自WWDC 2013 207 session 关于 Core Data 和 iCloud:

您在应用程序的本地中向我们提供了一个单独的商店 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 提供的存储)来解决此问题的示例。这对我来说是重复工作的味道。

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

这种方法怎么样?

  • 始终使用 Core Data 和 iCloud,无论 iCloud 是打开还是关闭。
  • 当 iCloud 从关闭变为打开时,询问用户是否要将本地帐户与 iCloud 帐户合并。如果是,则合并、删除优先于本地的重复项并清空本地帐户。
  • 当 iCloud 从开启到关闭时,询问用户是否要将 iCloud 商店与本地帐户合并。如果是,则合并并删除优先于 iCloud 的重复项。

这类似于 提醒 所做的。但是,提醒会直接从 iCloud 设置中询问用户有关数据迁移的问题,这是我们开发人员无法做到的。

问题

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

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

3) 你会在NSPersistentStoreCoordinatorStoresWillChangeNotificationNSPersistentStoreCoordinatorStoresDidChangeNotification 之间进行用户提示和合并,还是收集其中的所有信息并等到商店更改?我之所以问是因为这些通知似乎是在后台发送的,阻止它们执行可能需要很长时间的操作可能不是 Core Data 所期望的。

【问题讨论】:

  • 祝你好运。我花了一周的时间试图完成与您概述的类似的工作。最后,我想不出一个可靠的解决方案来保证用户不会丢失任何数据。我只是使用本地存储和 iCloud 仅用于备份存储。
  • @user523234 您介意发布您遇到的问题吗? “这是一个坏主意,因为......”也是一个有效的答案。
  • 有关 iCloud 帐户更改的正确通知是 NSUbiquityIdentityDidChangeNotification,但是如果您的应用程序未运行,您可以依靠收到此通知。 NSPersistentStoreCoordinator 通知不仅仅与 iCloud 转换相关。有关更详细的说明,请参阅下面我的答案中的链接。 UIManagedDocument 和 iCloud 集成页面上有一个非常详细的解释 ossh.com.au/design-and-technology/software-development/…
  • 只有我认为 Apple 解决方案的复杂性很荒谬吗?在 iOS 7 之前的大约一年里,我尝试采用 Apple 的解决方案,但没有成功。即使我可以接近,API 也非常复杂。我的建议是看看其他选项,包括 Wasabi Sync 和 Ensembles。他们只是合并云和本地数据,这通常是用户所期望的。 (披露:我开发合奏)
  • @DrewMcCormack 我给 Ensembles 看了一眼。感谢开源它。鉴于您的经验,您介意看看我的答案,看看您是否能发现任何问题?

标签: ios macos core-data icloud core-data-migration


【解决方案1】:

我认为您误解了 207 会议中所说的内容。

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

因为当您的应用首次安装时,新应用数据和文档的默认安全设置为开启,您必须询问用户是否要使用 iCloud。试试 Apple 的 Pages 应用。

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

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

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

您只需实现您的遗嘱更改处理程序并响应 NSPersistentStoreCoordinator 商店将发生变化并通知您 当我们需要更改持久存储文件时自动自动执行,因为 系统上有新帐户。

当然,你可以然后调用 NSManagedObjectContext 保存并 NSManagedObjectContext 重置。

现在,一旦您完成此操作,我们将从协调器中删除商店 与异步设置过程一样,然后我们将向您发送 NSPersistentStoreCoordinator Storage Did Change 通知,再次, 就像异步设置一样,您可以开始使用您的 像往常一样申请。

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

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

【讨论】:

  • 我将引用 WWDC 的演讲:“您在应用程序的本地沙箱中为我们提供了一个单独的商店 URL,然后我们为系统上的每个帐户创建一个包含条目的不透明容器,包括本地帐户,这是我们对系统上没有 iCloud 帐户时发生的情况的术语。这是一个由 Core Data 管理的特殊存储,因此您不必做任何特别的事情,因为您的用户没有没有 iCloud 帐户。”这与我的测试一致。
  • 关于 Pages,我认为确认 iCloud 的使用对于基于文档的应用程序比基于数据的应用程序更有意义,因为用户可能不太关心数据的当前位置。提醒可能是一个更合适的例子。然而,因为它是一个 Apple 应用程序,他们处理来自 iCloud 设置的迁移和用户提示,这有点不公平,因为我们不能这样做。
  • 我已编辑问题以反映此讨论。感谢您的意见。
  • 我还添加了该会话的一些摘录。
  • 哦,至于“使用 iCloud”设置 - 这也直接来自 Apple。我添加了一个指向 Tim Roadley 勘误表的链接,这也很有用,因为他解释了为什么需要使用 Settings Bundle。
【解决方案2】:

我将尝试回答我自己的问题,部分是为了整理我的想法,部分是为了回复@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];

其实只听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
    });
}

然后:

- (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进行。此逻辑将取决于您的模型以及重复和冲突策略。

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

【讨论】:

  • 问题是当您尝试进行迁移时,您无法保证存储文件确实存在。那么当应用程序不存在时,它会做什么呢?
  • 您将如何让用户能够真正从 iCloud 中删除内容?您需要使用 removeUbiquitousContentAndPersistentStoreAtURL API,我认为这仅在用户登录到 iCloud 时才有效。使用设置包允许您在本地移动内容,然后在注销 iCloud 之前从 iCloud 中删除所有内容。仅使用安全设置不会。
  • 优雅地通知用户没有数据丢失,他们需要重新打开 iCloud 才能查看。当他们关闭 iCloud 时,这只是一个(潜在的)问题,因为本地帐户存储必须始终存在,因为它不是 iCloud 支持的。如果 iCloud 帐户存储消失,那么您的解决方案或我的解决方案都无能为力。还是您的以某种方式解决了这个问题?
  • 事实上,当关闭文档和数据时,用户会收到一条消息,告诉他们所有 iCloud 数据都将从设备中删除,他们不会因为 iCloud 数据神秘地静止而感到困惑可用的。更糟糕的是,当他们重新打开 iCloud 时,您将不得不处理基本上整个商店的重复数据删除,或者必须询问用户是否要将本地数据与 iCloud 中已有的数据合并,或者他们是否想要使用 iCloud 数据或是否将 iCloud 数据替换为本地数据。并非没有一些挑战
  • 按顺序回答。 “你将如何让用户能够真正从 iCloud 中删除内容?”这可以是自定义设置是。我不反对这一点。我只是反对添加自定义设置来打开和关闭 iCloud,因为它已经存在。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-27
  • 2013-12-15
  • 2020-09-18
  • 1970-01-01
  • 2021-08-11
相关资源
最近更新 更多