UbiquityStoreManager 1.0.0

UbiquityStoreManager 1.0.0

测试测试
语言语言 Obj-CObjective C
许可 自定义
发布最后发布2014年12月

未声明 维护。



  • 作者
  • Maarten Billemont

关于

UbiquityStoreManager 是一个控制器,为您实现了 Core Data 与 iCloud 集成,并消除了所有困难。

当苹果第一次发布了惊人的 Core Data 和 iCloud 集成时,它被视为极度易整合的 API。不幸的是,正是相反。自那时起,苹果在 iOS 7 中取得了显著的进步。尽管如此,仍有许多需要处理的潜在问题、副作用和未记录的行为,以获得可靠的实现。

UbiquityStoreManager 还提供了一套极具实用性的迁移工具,对于那些希望在用户数据方面有比“默认”的选择更多的控制权的开发者来说。

不幸的是,在 iOS 6 上的用户仍然受到 iCloud 中许多严重错误的困扰,有时会导致云存储不同步,甚至完全损坏。UbiquityStoreManager 会尽可能地处理这些情况。

在保持 API 尽可能简单的同时,为应用开发者提供了所需的钩子以获取所需的确切行为。在可能的地方,UbiquityStoreManager 实现了安全和合理的默认行为来处理异常情况,但如果需要,也可以让您做出不同的选择。这些情况已在 API 文档中详细说明,以及您连接到管理者并实现自定义行为的能力。

免责声明

我免费提供 UbiquityStoreManager 和其示例应用程序,对此可能对您的应用造成的影响不负任何责任。

创建 UbiquityStoreManager 做出巨大的工作量,至今很少有开发者敢于尝试解决苹果留给我们的 iCloud for Core Data 问题。此代码免费提供,希望它在当前形式或其他形式中有助于您。如果您觉得这个解决方案有用,请考虑表示感谢或捐款。

Click here to lend your support to: UbiquityStoreManager and make a donation at www.pledgie.com !

关于 iOS 7 的说明

在iOS 7中,苹果显著提高了Core Data与iCloud的集成。希望从这些改进中受益的应用程序需要更新为仅支持iOS 7。已经更新了支持iOS 7的UbiquityStoreManager。运行iOS 7+(或OS X 10.9+)的设备将首先将旧数据迁移到新的iOS 7+专用存储,之后将同步到其他iOS 7+/OS X 10.9+设备。iOS 6/OS X 10.8设备将类似于只与自身同步。USM采用这种方式来隔离旧的具有缺陷的iCloud实现与改进的新实现。

随着iOS 7的改进,为什么你仍然需要使用USM呢?

  • iOS 6支持:苹果现在建议所有使用iCloud的应用仅支持iOS 7。你可能不会完全同意这一点。
  • 本地存储:为了简单起见,苹果决定当用户在设置中启用iCloud后,所有支持iCloud的应用将切换到该iCloud帐户。更重要的是,当用户想要停止使用iCloud时,所有数据都将丢失。USM为你提供了一个应用程序切换按钮,让用户在全局存储或非全局存储之间切换。
  • 迁移工具:USM 附带大量工具,可用于在存储之间迁移。其API简单易用,解决了你应用将面临的问题:如果你的用户想要停止使用iCloud,关闭iCloud开关可以通过一个调用导致USM切换到本地存储,并且如果用户选择,可以将他的云数据带到本地存储。
  • iCloud仍然不是微不足道的:你仍然需要做好很多事情。只需看看USM庞大的代码库即可。如果你使用USM,你所做的就是-init一个对象,设置一个代理,等待我们为你提供一个已完全初始化的NSPersistentStoreCoordinator。让我们来处理这项工作,这样你就可以专注于你的数据模型和应用。

入门

要开始使用UbiquityStoreManager,你所要做的就是实例化它

[[UbiquityStoreManager alloc] initStoreNamed:nil
                      withManagedObjectModel:nil
                               localStoreURL:nil
                         containerIdentifier:nil
                      additionalStoreOptions:nil
                                    delegate:self]

可以使用nil参数来自定义UbiquityStoreManager的行为。例如,如果你已经有一个本地存储,你可以将它的URL作为localStoreURL传递。

然后在你的代理中等待管理器设置你的持久化层

- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager willLoadStoreIsCloud:(BOOL)isCloudStore {

    self.moc = nil;
}

- (void)ubiquityStoreManager:(UbiquityStoreManager *)manager didLoadStoreForCoordinator:(NSPersistentStoreCoordinator *)coordinator isCloud:(BOOL)isCloudStore {

    self.moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [moc setPersistentStoreCoordinator:coordinator];
    [moc setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
}

就是这样!管理器设置了你的NSPersistentStoreCoordinator,你创建了一个NSManagedObjectContext,你可以开始了。

请记住,正如上面的代码所显示的,你的可以是nil。这发生在管理器尚未准备好加载存储时。也可能在存储加载后发生(例如,cloud处于开启或关闭状态,或者存储需要重新加载)。因此,请确保你的应用能够优雅地处理你的主要不可用。你还可以观察UbiquityManagedStoreDidChangeNotification,该操作每次持久化存储的可用性发生变化时都会发布(并且始终在通知你的代理之后)。

最初,管理器将使用本地存储。要启用iCloud(你可以在用户开启iCloud之后做这个),只需切换开关

manager.cloudEnabled = YES;

如果你喜欢,可以使用-setCloudEnabledAndOverwriteCloudWithLocalIfConfirmed:-setCloudDisabledAndOverwriteLocalWithCloudIfConfirmed:方法,这些方法允许你通过确认块向用户询问(在移动到新的(本地或云)存储时)是否希望携带他的当前数据。如果需要,确认块才会触发(新存储已经存在)。如果新存储还不存在,数据将默认迁移。

当然了,这还远未结束!

这取决于你想要多大程度地参与到UbiquityStoreManager内部如何处理你的存储,以及你想要多大程度地向用户提供关于正在发生的事情的反馈。

例如,当持久性不可用时,你可能希望实现可见反馈(例如,显示一个带有加载轮子的覆盖层)。你可以在ubiquityStoreManager:willLoadStoreIsCloud:中显示这个轮子,并在ubiquityStoreManager:didLoadStoreForCoordinator:isCloud:中将其关闭。

还可能是一个好主意,在你的主moc中更新,每当您从其他设备导入持久性更改时。为此,只需通过从managedObjectContextForUbiquityChangesInManager:返回它来为管理员提供您的moc,并可选地注册一个观察者用于UbiquityManagedStoreDidImportChangesNotification

如果事情出错呢?

不要被误导:事情确实可能出错。对于iOS 6/OS X 10.8用户来说,还有一些问题,这些问题可能会导致云存储成为无法修复的脱节。

UbiquityStoreManager尽力应对这些问题,主要是自动的。由于管理员非常小心以确保没有数据丢失,有些情况下存储无法自动恢复。因此,实施一些失败处理非常重要,至少按照管理员推荐的方式。

虽然在理论上不应该发生,但有时旨在同步您的云存储与其他设备存储的持久性更改可能与您的云存储不兼容。通常,这种情况是由于苹果iCloud在处理同时来自不同设备编辑的关系时遇到的苹果iCloud错误,这导致无法处理的冲突。有趣的是,错误发生在苹果iCloud实现的最深处,苹果没有通过任何公开的API通知您。UbiquityStoreManager实现了一种在发生这些问题时检测这些问题并尽可能解决它们的方法。

每当导入事务日志(持久性更改)出现问题,您的应用程序都可以收到通知,并可选择通过在您的代理中实现ubiquityStoreManager:handleCloudContentCorruptionWithHealthyStore:来介入。如果您只想被告知,然后让管理员处理情况,请返回NO。如果要以不同于管理员默认方式的方式处理情况,请先处理问题后再返回YES

本质上,经理通过卸载与存储冲突的持久性更改的设备的存储,并通知所有其他设备存储已经进入了一个<强>“损坏”状态来处理导入异常。其他设备可能不会遇到任何错误(它们可能是损坏日志的作者,或者它们可能没有在其存储和日志之间遇到冲突)。当任何这些<强>“健康”设备收到存储损坏的消息时,它们将启动存储重建,创建一个新的云存储,其中包含旧的云存储的实体。此时,所有设备都将切换到新的云存储,损坏状态将清除。

建议您在返回NO的同时通知用户正在进行什么操作。以下是一个实现示例,它在需要设备等待另一个设备修复损坏时,会向用户提供一个警报。

- (BOOL)ubiquityStoreManager:(UbiquityStoreManager *)manager
        handleCloudContentCorruptionWithHealthyStore:(BOOL)storeHealthy {

    if (![self.cloudAlert isVisible] && manager.cloudEnabled && !storeHealthy)
        dispatch_async( dispatch_get_main_queue(), ^{
            self.cloudAlert = [[UIAlertView alloc]
                    initWithTitle:@"iCloud Sync Problem"
                          message:@"\n\n\n\n"
                @"Waiting for another device to auto‑correct the problem..."
                         delegate:self
                cancelButtonTitle:nil otherButtonTitles:@"Fix Now", nil];
            UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc]
                    initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
            activityIndicator.center = CGPointMake( 142, 90 );
            [activityIndicator startAnimating];
            [self.cloudAlert addSubview:activityIndicator];
            [self.cloudAlert show];
        } );

    return NO;
}

上述代码给用户提供了点击Fix Now按钮的选项,这将调用[manager rebuildCloudContentFromCloudStoreOrLocalStore:YES]。本质上,它在本地启动云存储重建。有关此内容的更多信息稍后介绍。

您的应用程序现在可以处理苹果的iCloud缺陷,恭喜您!

除非您想深入研究,否则就到这里为止。你现在已经完成了。以下内容是给勇敢的人或在寻求最大控制的人准备的。

还有其他什么吗?

UbiquityStoreManager 尽最大努力让感兴趣的代理了解正在发生的事情,甚至允许它以非标准方式干预。

如果您使用日志记录器,可以通过实现 ubiquityStoreManager:log: 来连接。每当管理器有事情要说的时候都会调用该方法。我们非常健谈,所以您甚至可能只想为了在生产环境中关闭管理员而实现它。

如果您对获取有关任何错误条件的详细信息感兴趣,请实现 ubiquityStoreManager:didEncounterError:cause:context:,您将收到通知。

如果云内容被删除,则管理器将卸载持久存储。这可能在用户进入 设置 并删除您的应用的iCloud数据时发生,可能是为了在其iCloud账户上腾出空间。默认情况下,这将使您的应用在没有用户重新启动应用的情况下没有任何持久性。如果应用中仍然启用了iCloud,则会为他创建一个新的存储。您可以根据什么认为是对的来稍微不同地处理它:您可能只想向用户显示一条消息,询问他是否希望禁用或重新启用iCloud。或者您可能只想禁用iCloud并切换到本地存储。您会从 ubiquityStoreManagerHandleCloudContentDeletion: 处理这一点。

如果您仔细阅读了上一部分,应该明白在导入其他设备所做的无处不在的更改期间可能会出现问题。通常,处理这种情况的默认方式可以自动解决问题,但可能需要一些时间才能完全解决,可能还需要用户交互。您可以选择通过实现 ubiquityStoreManager:handleCloudContentCorruptionWithHealthyStore: 并在处理完损坏后返回 YES 来以不同的方式处理这种情况。管理器为您提供以下方法,您可以使用这些方法对存储进行一些低级别的维护:

  • -reloadStore — 仅清除并重新打开或重试打开活动存储。
  • -setCloudEnabledAndOverwriteCloudWithLocalIfConfirmed: — 如果尚未启用,则启用iCloud。如果用户已经有了一个iCloud存储,您的确认块将被调用,允许您询问用户是否想要切换到现有的云数据或用本地数据覆盖它。
  • -setCloudDisableAndOverwriteLocalWithCloudIfConfirmed: — 如果已启用,则禁用iCloud。如果用户已经有了一个本地存储,您的确认块将被调用,允许您询问用户是否想要切换到现有的本地数据或用云数据覆盖它。
  • -migrateCloudToLocal — 手动用 iCloud 存储中的数据覆盖用户的本地数据。
  • -migrateLocalToCloud — 手动用本地存储中的数据覆盖用户的云数据。
  • -deleteCloudContainerLocalOnly: — 将删除您应用的所有iCloud数据。这不仅是您的Core Data存储
  • -deleteCloudStoreLocalOnly: — 将删除您的Core Data云存储。
  • -deleteLocalStore — 这将删除您的本地存储(即 manager.cloudEnabledNO 时活动的存储)。
  • -rebuildCloudContentFromCloudStoreOrLocalStore: — 这就是云存储重建魔法的所在地。调用此方法以创建一个新的云存储并将当前云数据复制到其中。

许多这些方法都包含一个 localOnly 参数。将其设置为 YES,如果您不想影响用户的所有iCloud数据。操作将在本地设备上发生。例如,如果您运行 [manager deleteCloudStoreLocalOnly:YES],则设备上的云存储将被删除。如果 cloudEnabledYES,则管理器将随后重新打开云存储,这将导致重新下载商店的所有iCloud事务日志。然后,这些事务日志将在本地重演,您的本地存储将从iCloud中的数据中重新填充。

USM 将始终尽可能地不被破坏。如果迁移或操作失败,它将将用户回滚到变更之前的状态。

parseLogs

parseLogs bash 脚本允许您分析苹果的详尽 ubiquity 日志输出并提供一些反馈信息。它处于非常初级阶段,但旨在帮助调试与任何 iCloud 相关的同步问题。

parseLogs screenshot

要使用脚本,只需运行它(在脚本所在目录下或在将 bashlib 依赖项复制到 PATH 后),将 ubiquity 日志通过 STDIN 传入。

./parseLogs < myapp.logs

要使其更加详尽,请添加 -v 选项。详尽输出将显示未处理的日志行。本脚本的目标是处理苹果的 ubiquity 日志输出的所有日志行。您可以通过修改脚本或 LOGS 摘要文件来做出贡献。

内部机制

UbiquityStoreManager 努力隐藏所有细节,为您维护持久存储管理器。如果您对它所做的一切以及这些是如何运作的感兴趣,请继续阅读。

术语

  • USM = UbiquityStoreManager,本项目。
  • PSC = Persistent Store Coordinator,在存储文件、存储模型和管理上下文之间协调的对象。
  • MOC = Managed Object Context,对数据存储的“视图”。每个上下文都有自己关于存储对象外观的内存中概念。

核心理念

USM 背后的理念是创建一个实现,隐藏所有加载持久存储的细节,允许应用专注于简单地使用它。

为了实现这一点,USM 仅暴露了应用开始使用商店时所需的最小 API,以及一些可能需要用于处理非默认情况的应用的低级 API。USM 实现了合理的默认行为,但允许应用在需要时做出不同的选择。

关于如何设置和配置持久层,有几种不同的方法。USM 已经为您做出了以下选择/假设:

  • 用户 iCloud 数据和本地数据之间有区别。
    • 当 iCloud 禁用 时,将加载一个独立的本地存储。用户将看不到他们的 iCloud 数据。
    • 当 iCloud 启用 时,将加载 iCloud 存储。这个存储最初将是本地存储的副本,但任何更改都不会保存在本地存储中:它们只会存在于云上。
  • 是否启用 iCloud 是一个 设备特定 的选择:在单个设备上启用 iCloud 并不会导致所有设备突然切换到 iCloud。
    • 建议应用程序开发者在应用程序首次启动时询问用户是否想启用 iCloud,并允许他们通过设置开关。
  • 当用户启用 iCloud 而尚未建立云存储时,将创建一个新的 iCloud 存储,并通过本地存储中的数据(填充)对其进行初始化。
  • 当用户同时在我的设备和两台设备上启用 iCloud 时,最后完成云存储初始化的我的设备将获胜,并且两台设备都将开始使用获胜的云存储。
  • 当用户启用 iCloud 而已经存在一个活动的云存储时,设备将开始使用现有的云存储。
    • 为了允许用户从新设备重新初始化云存储,USM 提供了用于删除云存储的实用工具。删除云存储后,启用 iCloud 将导致从本地存储再次初始化新的存储。
  • 当用户手动从其iCloud帐户中删除云存储(例如通过设备的设置)时,USM将对其进行清理并切换回用户的本地存储。用户明显是想删除云中的数据。
    • 存在一个应用程序钩子,允许应用程序决定如何处理这种情况,如果它们不想让USM切换到本地存储。
  • 如果用户在设备上注销其iCloud帐户或切换到另一个用户的iCloud帐户,USM会立即卸载云存储。在后一种情况下,将从本地存储创建一个新的iCloud存储。

工作原理

初始化时,USM将开始加载存储。在加载任何存储之前,应用程序的委托首先通过willLoadStoreIsCloud:被通知。在此阶段,应用程序仍然可以对其进行任何更改,并且由于此方法是从内部持久性队列中调用的,实际上这是执行USM任何初始配置的**推荐**位置。不要从调用USM的init方法的那个方法中这样做。

cloudEnabled属性确定将加载哪个存储。如果设置为YES,USM将开始加载云存储。如果设置为NO,则加载本地存储。

加载本地存储的过程相对简单。如果尚未存在,则会创建存储文件的目录层次结构以及存储文件本身。启用自动轻量级存储模型迁移和映射推断。您可以通过USM的init方法指定额外的存储加载选项,例如文件安全性选项。如果存储成功加载,应用程序将收到通知,并通过didLoadStoreForCoordinator:isCloud:收到访问存储所需的PSC。如果存储由于某些原因无法加载,应用程序将无法访问持久性,并通过failedLoadingStoreWithCause:context:wasCloud:获得通知。它可以选择以某种方式处理此故障。

加载云存储的过程略为复杂。主要与本地存储相同,但有一些额外的逻辑。

  • 如果云内容已不存在,但本地云存储文件仍然存在,则假定用户想要删除其云数据,并将本地存储文件删除。
  • 如果不存在云内容,将创建一个新的云存储,通过迁移本地存储的内容。
    • 此新云存储通过一个随机UUID进行标识。
    • 在新存储迁移和打开过程成功之前,将保留此新UUID。
    • 成功后,将通过使其UUID无处不在将其标记为“活动”云存储。
  • 如果云存储一旦加载失败,将尝试恢复,这会删除本地云存储文件并重新打开云存储,允许iCloud通过再次导入所有云内容来重新初始化本地云存储文件。
  • 如果云存储继续加载失败,则将存储标记为“损坏”。
  • 如果存储成功加载,USM等待20秒(以查看是否有云内容将无法导入),如果未检测到任何失败,则检查其他设备是否已报告云存储为“损坏”。如果是这样,则从该设备启动云内容恢复,该设备由于在加载存储方面的成功而被认为很健康。

加载存储时,USM会监视其是否被删除。当云存储被删除时,USM将清理任何“损坏”标记、本地云存储文件,并回退到本地存储,除非应用程序选择通过ubiquityStoreManagerHandleCloudContentDeletion:处理这种情况。当本地存储被删除时,USM将重新加载,导致创建新的本地存储。

cloudEnabled 设置存储在 NSUserDefaults@"USMCloudEnabledKey" 键下。当 USM 检测到此键的值发生变化时,它将重新加载当前存储,使得更改生效。您可以利用这一点在 Settings.bundle 中为 iCloud 添加偏好设置。

每次应用程序变得活跃时,USM 都会检查 iCloud 身份是否发生变化。如果检测到变化且 iCloud 目前已启用,则存储将被重新加载,以使得更改生效。类似地,当检测到活动无处不在存储的 UUID 发生变化且 iCloud 目前已启用时,存储也会被重新加载。

当检测到云端存储中的无处不在变化时,应用程序的代理可以指定一个自定义 MOC 来用于导入这些更改,以便它可以立即了解这些更改。为此,应用程序应通过 managedObjectContextForUbiquityChangesInManager: 返回其 MOC。如果无处不在更改失败导入,则存储将被重新加载以重试该过程并验证是否发生任何损坏。成功完成后,将发布 UbiquityManagedStoreDidImportChangesNotification 通知。

当云端存储无法加载或云事务日志无法导入时,云存储会被标记为“损坏”。为了检测 Apple 的 Core Data 在事务日志导入尝试中的失败,USM 绕过 NSError 的初始化方法。这样,它可以检测到当创建针对事务日志导入失败的 NSError 时发生的操作,并据此采取相应的行动。

当检测到云端“损坏”时,立即卸载云存储以防止进一步的去同步。当存储当前设备上的存储不健康(存储加载失败或导入无处不在更改失败),USM 将等待,持久化层保持不可用。当存储在当前设备上是健康的,USM 将通过从 iCloud 删除云端内容并创建一个新的云存储(具有新的随机 UUID)来启动云端内容的重建,该 UUID 将从健康的本地云存储文件中播种。新云存储将填充旧云存储的数据,并为其建立健康的云内容。完成后,等待的不健康的设备将注意到一个新云存储变得活跃,加载它,并再次变得健康。应用程序可以通过实现 handleCloudContentCorruptionWithHealthyStore: 来钩入此过程,并修改发生的操作。

任何存储迁移都可以通过以下四种策略之一执行

  • UbiquityStoreMigrationStrategyIOS:此策略通过协调 PSC 的 migratePersistentStore: 执行迁移。一些 iOS 版本在这方面存在错误,使其通常不可靠。这是 iOS 7 上的默认策略。
  • UbiquityStoreMigrationStrategyCopyEntities:此策略通过复制任何非无处不在元数据和复制来自一个存储到另一个存储的所有实体、属性和关系来执行迁移。这是 iOS 6 的默认策略。
  • UbiquityStoreMigrationStrategyManual:此策略允许应用程序的代理通过实现 manuallyMigrateStore: 来执行迁移。如果您有一个非常大的或复杂的数据模型,或者想要更有控制地迁移实体,这可能很有必要。
  • UbiquityStoreMigrationStrategyNone:不执行任何迁移,并且以当前的形式打开新存储。

许可协议

UbiquityStoreManager 根据 Apache 许可协议,版本 2.0 许可。

请随意将其用于您的任何应用程序。我也很高兴收到任何评论、反馈或审查任何 pull 请求。