维护 CloudKit 记录的本地缓存
您可能希望将CloudKit记录的本地缓存添加到您的应用,以支持离线使用您的应用或提高性能。或者您可能已经为您的应用程序提供了数据存储,并且您希望添加对在CloudKit中持久存储该数据的支持。
一般工作流程
配置应用程序以维护本地缓存后,以下是您的应用将遵循的常规流程:
- 当您的应用首次在新设备上启动时,它将订阅用户私有和共享数据库中的更改。
- 当用户在设备A上本地修改其数据时,您的应用会将这些更改发送到CloudKit。
- 您的应用将在同一用户的设备B上收到推送通知,通知它已在服务器上进行了更改。
- 您在设备B上的应用程序将询问服务器自上次与服务器通信后发生的更改,然后使用这些更改更新其本地缓存。
初始化容器
应用程序启动时,应用程序的初始化逻辑应该会运行。无论您是否已经创建了区域和订阅,您的应用都应该在本地缓存,这样您就不会在每次启动时发出不必要的请求。
首先,代码定义了整个示例中要使用的项目。
let container = CKContainer.default()
let privateDB = container.privateCloudDatabase
let sharedDB = container.sharedCloudDatabase
// Use a consistent zone ID across the user's devices
// CKCurrentUserDefaultName specifies the current user's ID when creating a zone ID
let zoneID = CKRecordZoneID(zoneName: "Todos", ownerName: CKCurrentUserDefaultName)
// Store these to disk so that they persist across launches
var createdCustomZone = false
var subscribedToPrivateChanges = false
var subscribedToSharedChanges = false
let privateSubscriptionId = "private-changes"
let sharedSubscriptionId = "shared-changes"
创建自定义区域
要使用CloudKit的更改跟踪功能,您需要将应用程序数据存储在用户私有数据库的自定义区域中。您可以通过实例化CKModifyRecordZonesOperation对象来创建自定义区域,如下所示。
let createZoneGroup = DispatchGroup()
if !self.createdCustomZone {
createZoneGroup.enter()
let customZone = CKRecordZone(zoneID: zoneID)
let createZoneOperation = CKModifyRecordZonesOperation(recordZonesToSave: [customZone], recordZoneIDsToDelete: [] )
createZoneOperation.modifyRecordZonesCompletionBlock = { (saved, deleted, error) in
if (error == nil) { self.createdCustomZone = true }
// else custom error handling
createZoneGroup.leave()
}
createZoneOperation.qualityOfService = .userInitiated
self.privateDB.add(createZoneOperation)
}
订阅变更通知
您的应用需要订阅其他设备所做的更改。订阅会告知CloudKit您关注哪些数据,以便在数据发生变化时向您的应用发送推送通知。
您的应用程序需要创建两个数据库更改(CKDatabaseSubscription
对象)订阅,一个用于私有数据库,另一个用于共享数据库。
if !self.subscribedToPrivateChanges {
let createSubscriptionOperation = self.createDatabaseSubscriptionOperation(subscriptionId: privateSubscriptionId)
createSubscriptionOperation.modifySubscriptionsCompletionBlock = { (subscriptions, deletedIds, error) in
if error == nil { self.subscribedToPrivateChanges = true }
// else custom error handling
}
self.privateDB.add(createSubscriptionOperation)
}
if !self.subscribedToSharedChanges {
let createSubscriptionOperation = self.createDatabaseSubscriptionOperation(subscriptionId: sharedSubscriptionId)
createSubscriptionOperation.modifySubscriptionsCompletionBlock = { (subscriptions, deletedIds, error) in
if error == nil { self.subscribedToSharedChanges = true }
// else custom error handling
}
self.sharedDB.add(createSubscriptionOperation)
}
// Fetch any changes from the server that happened while the app wasn't running
createZoneGroup.notify(queue: DispatchQueue.global()) {
if self.createdCustomZone {
self.fetchChanges(in: .private) {}
self.fetchChanges(in: .shared) {}
}
}
这些订阅告诉CloudKit只要在您创建订阅的数据库中添加,修改或删除记录或区域,就可以在此设备上向您的应用发送推送通知。
您可能希望配置订阅以发送静默推送通知。这些通知唤醒您的应用,以便它可以获取更改,但应用程序不会向用户显示警报。
func createDatabaseSubscriptionOperation(subscriptionId: String) -> CKModifySubscriptionsOperation {
let subscription = CKDatabaseSubscription.init(subscriptionID: subscriptionId)
let notificationInfo = CKNotificationInfo()
// send a silent notification
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: [])
operation.qualityOfService = .utility
return operation
}
倾听推送通知
在配置应用程序以使用CloudKit时,您需要配置应用程序以侦听远程通知。
CKNotification(fromRemoteNotificationDictionary: dict)
在userInfo
字典上使用以确定您的应用程序接收的远程通知是否由a触发CKSubscription
。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
print("Received notification!")
let viewController = self.window?.rootViewController as? ViewController
guard let viewController = self.window?.rootViewController as? ViewController else { return }
let dict = userInfo as! [String: NSObject]
guard let notification:CKDatabaseNotification = CKNotification(fromRemoteNotificationDictionary:dict) as? CKDatabaseNotification else { return }
viewController!.fetchChanges(in: notification.databaseScope) {
completionHandler(.newData)
}
}
获取更改
应用程序启动或者收到一推后,您的应用程序使用CKFetchDatabaseChangesOperation然后CKFetchRecordZoneChangesOperation向服务器只更改自从上次更新它。
这些操作的关键是previousServerChangeToken
对象,它告诉服务器您的应用程序上次与服务器通话的时间,允许服务器仅返回自那时以来更改的项目。
首先,您的应用将使用a CKFetchDatabaseChangesOperation
来找出哪些区域已更改,并且:
- 收集新区域和更新区域的ID。
- 清除已删除区域中的本地数据。
以下是一些获取数据库更改的示例代码:
func fetchChanges(in databaseScope: CKDatabaseScope, completion: @escaping () -> Void) {
switch databaseScope {
case .private:
fetchDatabaseChanges(database: self.privateDB, databaseTokenKey: "private", completion: completion)
case .shared:
fetchDatabaseChanges(database: self.sharedDB, databaseTokenKey: "shared", completion: completion)
case .public:
fatalError()
}
}
func fetchDatabaseChanges(database: CKDatabase, databaseTokenKey: String, completion: @escaping () -> Void) {
var changedZoneIDs: [CKRecordZoneID] = []
let changeToken = … // Read change token from disk
let operation = CKFetchDatabaseChangesOperation(previousServerChangeToken: changeToken)
operation.recordZoneWithIDChangedBlock = { (zoneID) in
changedZoneIDs.append(zoneID)
}
operation.recordZoneWithIDWasDeletedBlock = { (zoneID) in
// Write this zone deletion to memory
}
operation.changeTokenUpdatedBlock = { (token) in
// Flush zone deletions for this database to disk
// Write this new database change token to memory
}
operation.fetchDatabaseChangesCompletionBlock = { (token, moreComing, error) in
if let error = error {
print("Error during fetch shared database changes operation", error)
completion()
return
}
// Flush zone deletions for this database to disk
// Write this new database change token to memory
self.fetchZoneChanges(database: database, databaseTokenKey: databaseTokenKey, zoneIDs: changedZoneIDs) {
// Flush in-memory database change token to disk
completion()
}
}
operation.qualityOfService = .userInitiated
database.add(operation)
}
接下来,您的应用会使用包含CKFetchRecordZoneChangesOperation
您刚刚收集的区域ID集的对象来执行以下操作:
- 创建和更新任何已更改的记录
- 删除不再存在的任何记录
- 更新区域更改令牌
以下是一些获取区域更改的示例代码:
func fetchZoneChanges(database: CKDatabase, databaseTokenKey: String, zoneIDs: [CKRecordZoneID], completion: @escaping () -> Void) {
// Look up the previous change token for each zone
var optionsByRecordZoneID = [CKRecordZoneID: CKFetchRecordZoneChangesOptions]()
for zoneID in zoneIDs {
let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = … // Read change token from disk
optionsByRecordZoneID[zoneID] = options
}
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zoneIDs, optionsByRecordZoneID: optionsByRecordZoneID)
operation.recordChangedBlock = { (record) in
print("Record changed:", record)
// Write this record change to memory
}
operation.recordWithIDWasDeletedBlock = { (recordId) in
print("Record deleted:", recordId)
// Write this record deletion to memory
}
operation.recordZoneChangeTokensUpdatedBlock = { (zoneId, token, data) in
// Flush record changes and deletions for this zone to disk
// Write this new zone change token to disk
}
operation.recordZoneFetchCompletionBlock = { (zoneId, changeToken, _, _, error) in
if let error = error {
print("Error fetching zone changes for \(databaseTokenKey) database:", error)
return
}
// Flush record changes and deletions for this zone to disk
// Write this new zone change token to disk
}
operation.fetchRecordZoneChangesCompletionBlock = { (error) in
if let error = error {
print("Error fetching zone changes for \(databaseTokenKey) database:", error)
}
completion()
}
database.add(operation)
}
上面的代码有几条关于将更改写入内存然后将这些更改刷新到磁盘的注释。一般流程如下。
对于数据库:
- 当被告知区域删除时,将其写入内存。
当被告知数据库的新变更令牌时,将该令牌写入内存,然后:
- 将所有内存中区域更改保留到磁盘(删除已删除的区域,并记录需要更改的区域列表),或
- 将区域删除保留到磁盘,并获取所有已修改记录区域的更改。
注意: 在获取数据库更改时,需要在获取数据库更改令牌之前保留所有收到的每个区域回调。
最后,将更新的数据库更改标记刷新到磁盘
同样,对于区域:
- 当被告知区域中的记录更改时,请将其写入内存。
- 当被告知区域的新变更令牌时,将该区域中的所有内存中记录更改以及该区域的更新变更令牌提交到磁盘。
存储记录元数据
要将本地数据存储中的记录与服务器上的记录相关联,您可能需要存储记录的元数据(记录名称,区域ID,更改标记,创建日期等)。有一个方便的方法CKRecord
,encodeSystemFieldsWithCoder,可以帮助您进行系统领域做到这一点。您仍然需要单独处理自己的自定义字段。
以下是您的应用如何读取元数据以便在本地存储它的示例:
// obtain the metadata from the CKRecord
let data = NSMutableData()
let coder = NSKeyedArchiver.init(forWritingWith: data)
coder.requiresSecureCoding = true
record.encodeSystemFields(with: coder)
coder.finishEncoding()
// store this metadata on your local object
yourLocalObject.encodedSystemFields = data
根据您的本地数据向CloudKit发送更改时,您可以将本地缓存读回CloudKit对象并操作它们以便在CloudKit中存储:
// set up the CKRecord with its metadata
let coder = NSKeyedUnarchiver(forReadingWith: yourLocalObject.encodedSystemFields!)
coder.requiresSecureCoding = true
let record = CKRecord(coder: coder)
coder.finishDecoding()
// write your custom fields...
高级本地缓存
用户可以通过iCloud设置 - >管理存储删除CloudKit服务器上的应用程序数据。您的应用程序需要优雅地处理此问题,如果它们不存在,请再次在服务器上重新创建区域和订阅。在这种情况下返回的具体错误是userDeletedZone
。
WWDC2015的高级NSOperations演讲中概述的操作依赖系统是管理CloudKit操作的好方法,以便检查帐户和网络状态,并在适当的时间创建区域和订阅。
网络连接可能随时消失,因此请确保正确处理networkUnavailable
任何操作中的错误。
监视网络可访问性,并在网络再次可用时重试该操作。
也可以看看
在WWDC 2016 CloudKit最佳实践视频中了解有关本地缓存的更多信息。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论