在 WWDC 2019 上,苹果推出了 Core Data with CloudKit API ,极大地降低了 Core Data 数据的云同步门槛。因为该服务关于开发者来说几乎是免费的,因而在之后的几年中,越来越多的开发者在运用中集成了该服务,并为用户带来了良好的跨设备、跨平台的运用体会。本文将对实时切换 Core Data 云同步状况的完成原理、操作细节以及注意事项进行探讨和阐明。

假如你对 Core Data with CloudKit 尚不了解,请阅览我写的 有关 Core Data with CloudKit 的系列文章

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

非实时切换

所谓非实时切换是指:对 Core Data 云同步状况的修正并不能立即收效,同步状况只要在运用再次冷启动后才会发生改变。假如对同步状况切换的实时性没有迫切的需求,那么应该以此种切换办法为首选。

不设置 cloudKitContainerOptions

开发者经过对 NSPersistentStoreDescription 的 cloudKitContainerOptions 属性进行设置,让 NSPersistentStoreDescription( 在 Data Model Editor 中经过 Configuration 创立 ) 中的耐久化存储与某个 CloudKit container 相关起来。假如咱们不对 cloudKitContainerOptions 进行设置( 或设置为 nil ),那么 NSPersistentCloudKitContainer 将不会在此 NSPersistentStoreDescription 上启用网络同步功用。咱们可以运用这一点来设置 NSPersistentCloudKitContainer 的同步状况。

因为对 NSPersistentStoreDescription 的设置必须在 loadPersistentStores 之前完成,因而运用此种办法进行的状况设置,一般会在运用的下次冷启动后收效( 理论上,也可以经过创立新的 NSPersistentCloudKitContainer 实例来完成,但在单 container 的情况下,为了确保保管对象上下文中数据的完整性,需要照顾太多的或许性,难度较高 )。

lazy var container:NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "Model")
    let enableMirror = UserDefaults.standard.bool(forKey: "enableMirror")
    if enableMirror {
        container.persistentStoreDescriptions.first?.cloudKitContainerOptions = .init(containerIdentifier: "YourCloudKitContainerID")
    }
    // 其他设定
    container.loadPersistentStores{ desc,error in
        // ..
    }
    // 其他设定
    return container
}()

统一成 NSPersistentContainer

假如你的运用只运用了同步私有数据库的功用,那么也可以运用 NSPersistentCloudKitContainer 是 NSPersistentContainer 的子类这一事实,来达到相似的目的:

lazy var container1:NSPersistentContainer = {
    let container:NSPersistentContainer
    let enableMirror = UserDefaults.standard.bool(forKey: "enableMirror")
    if enableMirror {
        container = NSPersistentCloudKitContainer(name: "Model")
        container.persistentStoreDescriptions.first?.cloudKitContainerOptions = .init(containerIdentifier: "YourCloudKitContainerID")
    } else {
        container = NSPersistentContainer(name: "Model")
    }
    // 其他设定
    container.loadPersistentStores{ desc,error in
        // ..
    }
    // 其他设定
    return container
}()

NSPersistentCloudKitContainer 是怎么运作的

在介绍怎么完成实时切换同步状况之前,咱们首先需要对 NSPersistentCloudKitContainer 的构成和工作机制有所了解。

NSPersistentCloudKitContainer 由如下几个功用模块所构成:

NSPersistentContainer

NSPersistentCloudKitContainer 是 NSPersistentContainer 的子类,拥有 NSPersistentContainer 的全部能力。除了少量用于同享和公共数据鉴权 API 之外,开发者几乎百分百地只与 NSPersistentCloudKitContainer 中 NSPersistentContainer 部分打交道。因而从大的视点划分,NSPersistentCloudKitContainer 就是 NSPersistentContainer 加上网络处理部分。

Persistent History Tracking 处理 + 格局转化模块

经过默许启用 Persistent History Tracking 支撑,NSPersistentCloudKitContainer 可以获悉运用在 SQLite 上的一切操作,然后将数据转化成 CloudKit 对应的格局,并保存在 SQLite 上的特定表中( ANSCKEXPORT…、ANSCKMIRROREDRELATIONSHIP 等 ),待网络同步模块将其同步( Export )到云上。

相同关于从云上同步( Import )过来的数据,该模块会将其转化成 Core Data 对应的格局,并修正在 SQLite 中对应的数据。全部的修正操作将以 NSCloudKitMirroringDelegate.import( Transaction author )的身份记载在 Persistent History Tracking 的 Transaction 数据中。

因为该过程是在由 NSPersistentContainer 上创立的私有上下文中进行的,因而只需要将 viewContext.automaticallyMergesChangesFromParent 设置为 true ,即可完成数据在视图上下文中的主动兼并,而无需对 Persistent History Tracking 创立的 Transaction 做处理。

经过运用 Persistent History Tracking 这一支撑跨进程等级的数据修正提示机制,让 NSPersistentContainer 与网络同步功用之间形成了解耦。

有关 Persistent History Tracking 方面的内容,请参阅 在 CoreData 中运用耐久化前史盯梢 一文。想了解 Core Data 是如安在 SQLite 上组织数据的,请参阅 Core Data 是如安在 SQLite 中保存数据的 一文

网络同步模块

关于 Export 数据,该模块将择机( 视网络情况、数据更新频率等 )将转化后的数据上传到 iCloud 上。

关于 Import 数据,该模块在取得云端数据改变告诉后( 经过敞开 Remote notifications ),会将网络端的改变数据保存到 SQLite 中,供转化模块运用。

一切的网络同步操作都将以日志的形式保存在 SQLite 中。在 iCloud 账户状况发生变化后,NSPersistentCloudKitContainer 将运用这些同步记载作为数据重置的凭据。

数据权限模块

在敞开 NSPersistentCloudKitContainer 的同步同享数据库或公共数据库功用后,为了进步数据操作权限的验证功率,该模块会将同享或公共数据库在 iCloud 上对应的原始数据( CKRecordType、记载令牌等 )备份在本地的 SQLite 中,并供给鉴权 API 供开发者调用。

实时切换的原理

NSPersistentCloudKitContainer 这种模块化的构成办法,为完成实时切换同步状况供给了根底。

经过创立双 container ( NSPersistentContainer + NSPersistentCloudKitContainer ),咱们将运用程序中关于 Core Data 的操作同网络同步功用别离开来。

两个 Container 都运用相同的 Data Model,并均敞开 Persistent History Tracking 功用以感知对方在 SQLite 上的数据修正操作。程序中有关数据业务逻辑的操作在 NSPersistentContainer 实例上进行,而 NSPersistentCloudKitContainer 实例仅担任数据的网络同步服务。

如此一来,经过启用或禁用担任网络同步的 NSPersistentCloudKitContainer 实例,便可完成对网络同步状况的实时切换。因为运用中一切的数据操作仅在 NSPersistentContainer 上进行,因而在运转中实时切换同步状况并不会对数据的安全性和稳定性形成影响。

理论上,运用一个未配置 cloudKitContainerOptions 的 NSPersistentCloudKitContainer 替代 NSPersistentContainer 也是可以的。但因为没有经过充沛测试,本文中仍将运用 NSPersistentContainer + NSPersistentCloudKitContainer 的组合

完成细节提示

可在此处获取基于以上分析创立的 演示代码

本节将根据演示代码对部分完成细节进行阐明

多个 Container 运用同一个 Data Model

在一个运用程序中,Core Data 的 Data Model( 运用数据模型编辑器创立的模型文件 )只能被加载一次。因而咱们需要在创立 container 前首先加载该文件并创立为 NSManageObjectModel 实例以供多个 container 运用。

private let model: NSManagedObjectModel
private let modelName: String
init(modelName: String) {
    self.modelName = modelName
    // load Data Model
    guard let url = Bundle.main.url(forResource: modelName, withExtension: "momd"),
          let model = NSManagedObjectModel(contentsOf: url) else {
        fatalError("Can't get \(modelName).momd in Bundle")
    }
    self.model = model
    ...
}
lazy var container: NSPersistentContainer = {
    // 运用 NSManagedObjectModel 来创立 container
    let container = NSPersistentContainer(name: modelName, managedObjectModel: model)
    ...
    return container
}()

这种办法在 把握 Core Data Stack 一文的内存形式章节中也有运用

将 NSPersistentCloudKitContainer 声明为可选值

经过将用于网络同步的 container 声明为可选值,即可轻松完成敞开和封闭同步功用:

final class CoreDataStack {
    var cloudContainer: NSPersistentCloudKitContainer?
    lazy var container: NSPersistentContainer = {
        let container = NSPersistentContainer(name: modelName, managedObjectModel: model)
        ...
        return container
    }
    init(modelName: String) {
        ....
        // 判别是否创立同步 container
        if UserDefaults.standard.bool(forKey: enableCloudMirrorKey) {
            setCloudContainer()
        } else {
            print("Cloud Mirror is closed")
        }
    }
    // 创立用于同步的 container
    func setCloudContainer() {
        if cloudContainer != nil {
            removeCloudContainer()
        }
        let container = NSPersistentCloudKitContainer(name: modelName, managedObjectModel: model)
        ....
        cloudContainer = container
    }
    // 删去用于同步的 container
    func removeCloudContainer() {
        guard cloudContainer != nil else { return }
        cloudContainer = nil
        print("Turn off the cloud mirror")
    }
}

两个 Container 上均需启用耐久化前史盯梢

只要在两个 container 均敞开 Persistent History Tracking 功用的情况下,它们才干感知到另一方对 Core Data 数据的修正行为,并进行处理。

container.persistentStoreDescriptions.first?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.persistentStoreDescriptions.first?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)

同时为了可以处理兼并冲突,两者都要设置正确的兼并战略:

container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

在 NSPersistentContainer 实例中呼应耐久化前史盯梢告诉

当 NSPersistentCloudKitContainer 实例从网络上获取到数据并更新到 SQLite 后,会在 SQLite 中创立 Transaction 并经过 NotificationCenter 发送 NSPersistentStoreRemoteChange 告诉。咱们需要在 NSPersistentContainer 实例中对该告诉进行呼应,并将同步数据兼并到当前的视图上下文中。

假如像本文例程中一样运用 Persistent History Tracking Kit 处理 Transaction 的话,需要敞开 includingCloudKitMirroring 选项以兼并由 NSPersistentCloudKitContainer 从网络上获取的改变数据:

persistentHistoryKit = .init(container: container,
                             currentAuthor: AppActor.app.rawValue,
                             allAuthors: [AppActor.app.rawValue],
                             includingCloudKitMirroring: true, // 兼并网络同步数据
                             userDefaults: UserDefaults.standard,
                             cleanStrategy: .none)

请参阅 在 CoreData 中运用耐久化前史盯梢 一文了解 Persistent History Tracking 的具体用法。有关 Persistent History Tracking Kit 的内容请参阅其附带的 ReadMe 文档

不要铲除 Transaction 记载

与仅在 App group 成员中运用 Persistent History Tracking 不同,在网络同步状况可以随时切换的情况下,最好不要铲除 Persistent History Tracking 功用创立的 Transaction 记载

这是因为 NSPersistentCloudKitContainer 是根据 Transaction 来判别哪些数据发生了改变,假定咱们在封闭了网络同步状况的情况下删去了 Transaction,敞开同步后,NSPersistentCloudKitContainer 将无法获悉在封闭期间本地数据发生的改变,然后会形成本地和云端数据的永久不同步。

之所以仅在 App group 成员间运用 Persistent History Tracking 可以删去 Transaction 记载,那是因为每个成员都会在兼并数据后,更新其对应的时刻戳。当进行 Transaction 删去操作时,咱们可以只删去现已被一切成员兼并过的记载。因为无法经过简单的办法得知 NSPersistentCloudKitContainer 的最终更新时刻以及已同步的数据方位,因而保存 Transaction 记载是最佳的选择

在本文的例程中,经过将 PersistentHistoryTrackingKit 的 cleanStrategy( 铲除战略 )设置为 none ,制止了对 Transaction 的铲除操作:

persistentHistoryKit = .init(container: container,
                             currentAuthor: AppActor.app.rawValue,
                             allAuthors: [AppActor.app.rawValue],
                             includingCloudKitMirroring: true,
                             userDefaults: UserDefaults.standard,
                             cleanStrategy: .none) // 不铲除 transaction

假如你的运用只会切换一次同步状况( 从封闭切换到敞开,并且之后不再封闭 ),那么可以在敞开同步状况后,对由你的 App group 成员发生的 Transaction 进行铲除。

怎么处理同享数据库和公共数据库的同步

鉴于 NSPersistentContainer 并没有供给数据鉴权方面的 API,在你的运用运用了同享数据库或公共数据库同步功用时,可以采用相似如下的办法来处理:

import CloudKit
final class CoreDataStack {
    let localContainer:NSPersistentContainer
    let cloudContainer:NSPersistentCloudKitContainer?
    var container:NSPersistentContainer {
        guard let cloudContainer else {
            return localContainer
        }
        return cloudContainer
    }
    // 某些权限检查工作,仅用于举例
    func checkPermission(id:NSManagedObjectID) -> Bool {
        guard enableMirror,let container = self.container as? NSPersistentCloudKitContainer else { return false}
        return container.canUpdateRecord(forManagedObjectWith:id)
    }
}

强烈建议在封闭网络同步状况的情况下,屏蔽掉运用中或许导致同享数据库和公共数据库进行修正操作的功用。

iCloud 账号状况改变的处理

本节介绍的内容会更改苹果有关 iCloud 数据一致性的预设行为,除非你清楚自己在做什么,也的确有这方面的特别需求,不然不要容易测验!

关于采用了 NSPersistentCloudKitContainer 进行数据同步的运用,当运用者在设备上退出 iCloud 账户、切换账户或者封闭运用的 iCloud 同步功用后,NSPersistentCloudKitContainer 会在重启后( 在运用运转中进行如上操作,iOS 运用会主动重启 )对一切的与账户相关的数据在设备上进行铲除( 并不会铲除云端的数据,当账户康复或敞开同步功用后仍可同步回本地 )。该铲除操作归于一种预设行为,是正常的现象。

某些体系运用供给了在 iCloud 账户退出后保存本地数据的能力。但 NSPersistentCloudKitContainer 默许并不供给保存数据的规划。

在重新启动后,NSPersistentCloudKitContainer 经过查询 CKContainer 的 accountStatus 取得 noAccount 状况,然后激活数据删去操作。删去操作是以上文中说到的网络同步模块中保存的数据同步日志为根据进行的。

假如,你想修正 NSPersistentCloudKitContainer 默许的数据处理行为,可以在创立 NSPersistentCloudKitContainer 实例之前,首先判别 CloudKit container 的 accountStatus,只在其不为 noAccount 状况时创立实例。例如:

import CloudKit
func setCloudContainerWhenOtherStatus() {
    let container = CKContainer(identifier: "YourCloudKitContainerID")
    container.accountStatus{ status,error in
        if status != .noAccount {
            self.setCloudContainer()
        }
    }
}

或者,在 accountStatus 为 noAccount 状况时,将 NSPersistentCloudKitContainer 的 NSPersistentStoreDescription 的 cloudKitContainerOptions 设置为 nil,然后屏蔽它的主动铲除行为。

假如咱们将本该主动铲除的数据保存在本地,且用户切换了 iCloud 账户,假如不做妥善处理的话,很或许会形成数据在多个账户之间的紊乱

总结

俗话说有得必有失,运用了双 container 以及不铲除 transaction 的办法完成对同步状况的实时切换,势必会带来些许的性能损失以及资源占用。不过,假如你的运用确有这方面的需求,这点支付仍是十分值得的。

Persistent History Tracking 现在现已越来越多地出现于各种场合,除了感知 App group 成员间数据变化外,还被运用于 数据批处理、数据云同步、Spotlight 等多个环节。建议 Core Data 的运用者应该对其有充沛的了解,并尽早将其运用于你的程序之中。

期望本文可以对你有所协助。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公共号:【肘子的Swift记事本】

我正在参加技能社区创作者签约方案招募活动,点击链接报名投稿。