Core Data with CloudKit

527 阅读4分钟

在应用程序中启用Core Data with CloudKit功能,只需要以下几步:

1. 使用NSPersistentCloudKitContainer

struct PersistenceController {
    static let shared = PersistenceController()
    let container: NSPersistentCloudKitContainer
    init(inMemory: Bool = false) {
        container = NSPersistentCloudKitContainer(name: "PMBok")
        guard let description = container.persistentStoreDescriptions.first else {
            print("Can't set description")
            fatalError("Error")
        }
        // 在该Description上启用Persistent History Tracking
        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        // 接收有关的远程通知
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        
        description.cloudKitContainerOptions?.databaseScope = .public
        
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        // 让视图上下文自动合并服务器端同步(import)来的数据/使用@FetchRequest或NSFetchedResultsController的视图可以将数据变化及时反应在UI上。
        container.viewContext.automaticallyMergesChangesFromParent = true
        // 设定合并冲突策略。如果不设置该属性,
        // Core Data会默认使用NSErrorMergePolicy作为冲突解决策略(所有冲突都不处理,直接报错),
        // 这会导致iCloud的数据无法正确合并到本地数据库。
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        do {
        // 避免在数据导入期间应用程序产生的数据变化和导入数据不一致而可能出现的不稳定情况。
              try container.viewContext.setQueryGenerationFrom(.current)
        } catch {
             fatalError("Failed to pin viewContext to the current generation:\(error)")
        }
    }
}

2. 在项目Target的Signing&Capablities中添加CloudKit支持

点击项目中对应的Target,选择Singing&Capabilities。点击+Capability查找icloud添加CloudKit支持。

16692680857002.jpg

3. 为项目创建或指定CloudKit container

16692680988338.jpg 勾选CloudKit。点击+,输入CloudKit container名称。Xcode会在你CloutKit container名称的前面自动添加iCloud.。container的名称通常采用反向域名的方式,无需和项目或BundleID一致。如果没有配置开发者团队,将无法创建container。

4. 在项目Target的Signing&Capablities中添加background支持

继续点击+Capability,搜索backgroud并添加,勾选Remote notifications

5. 配置NSPersistentStoreDescription以及viewContext

查看当前项目中的.xcdatamodeld文件,CONFIGURATIONS中只有一个默认配置Default,点击可以看到,右侧的Used with CloudKit已经被勾选上了。

6. 检查Data Model是否满足同步的要求

CloudKit Schema并不支持Core Data Model的所有功能、配置,因此在设计可同步的Core Data项目时,请注意以下限制,并确保你创建了一个兼容的数据模型。

Enitites CloudKit Sechma不支持Core Data的唯一限制(Unique constraints) Core Data的Unique constraints需要SQLite提供支持,CloudKit本身并非关系型数据库,因此不支持并不意外。

Attributes

不可以有即为非可选值又没有默认值的属性。允许:可选 、有默认值、可选 + 有默认值 上图中的属性 非Optional 且 没有Default Value是不兼容的形式,Xcode会报错。 不支持Undefined类型

Relationships

所有的relationship必须设置为可选(Optional) 所有的relationship必须有逆向(Invers)关系 不支持Deny的删除规则

Configurations

实体(Entity)不得与其他配置(Configuration)中的实体建立relationship 官方文档中这个限制我比较困惑,因为即使不采用网络同步,开发者也通常不会为两个Configuration中的实体建立relationship。

四种合并策略

container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy 设定合并冲突策略。如果不设置该属性,Core Data会默认使用NSErrorMergePolicy作为冲突解决策略(所有冲突都不处理,直接报错),这会导致iCloud的数据无法正确合并到本地数据库。 Core Data预设了四种合并冲突策略,分别为:

NSMergeByPropertyStoreTrumpMergePolicy 逐属性比较,如果持久化数据和内存数据都改变且冲突,持久化数据胜出

NSMergeByPropertyObjectTrumpMergePolicy 逐属性比较,如果持久化数据和内存数据都改变且冲突,内存数据胜出

NSOverwriteMergePolicy 内存数据永远胜出

NSRollbackMergePolicy 持久化数据永远胜出

使用自定义的NSPersistentStoreDescription

有些开发者喜欢自定义NSPersistentDescription(即使只有一个Configuration),这种情况下,需要显式为NSPersistentDescription设置cloudKitContainerOptions,

let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
cloudStoreDescription.configuration = "Cloud"
  
cloudStoreDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "your.containerID")

有选择的同步数据

在实际应用中,有某些场景我们想有选择性地对数据进行同步。通过在Data Model Editor中定义多个Configuration,可以帮助我们实现对数据同步的控制。配置Configuration非常简单,只需将Entity拖入其中即可。在不同的Configuration中放置不同的Enitity 假设以下场景,我们有一个Entity——Catch,用于作为本地数据缓存,其中的数据不需要同步到iCloud上。

我们创建两个Configuration: local——Catch cloud——其他需要同步的Entities

采用类似如下的代码:

let cloudURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
              .appendingPathComponent("cloud.sqlite")
let localURL = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first!
              .appendingPathComponent("local.sqlite")

let cloudDesc = NSPersistentStoreDescription(url: cloudURL)
cloudDesc.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "your.cloudKit.container")
cloudDesc.configuration = "cloud"

let localDesc = NSPersistentStoreDescription(url: localURL)
localDesc.configuration = "local"

container.persistentStoreDescriptions = [cloudDesc,localDesc]

只有Configuration cloud中的Entities数据会被同步到iCloud上。 我们不可以在跨Configuration的Entity之间创建relationship,如确有需要可以使用Fetched Preoperties达到受限的近似效果

私有库和公共库的区别

仅仅在这里

description.cloudKitContainerOptions?.databaseScope = .public