CloudSyncSession 是一个 Swift 库,它构建于 CloudKit 框架之上,旨在使编写启用同步、支持离线的应用程序更加容易。
与 NSPersistentCloudKitContainer 类似,CloudSyncSession 适用于需要在 iCloud 和客户端之间同步区域中所有记录的应用程序。与提供本地持久化的 NSPersistentCloudKitContainer 不同,CloudSyncSession 不以任何方式将状态持久化到磁盘。因此,它可以与任何本地持久化解决方案(例如 GRDB、Core Data、用户默认设置和文件存储等)结合使用。
static let storeIdentifier: String
static let zoneID: CKRecordZone.ID
static let subscriptionID: String
static let log: OSLog
static func makeSharedSession() -> CloudSyncSession {
let container = CKContainer(identifier: Self.storeIdentifier)
let database = container.privateCloudDatabase
let session = CloudSyncSession(
operationHandler: CloudKitOperationHandler(
database: database,
zoneID: Self.zoneID,
subscriptionID: Self.subscriptionID,
log: Self.log
),
zoneID: Self.zoneID,
resolveConflict: resolveConflict,
resolveExpiredChangeToken: resolveExpiredChangeToken
)
session.appendMiddleware(
AccountStatusMiddleware(
session: session,
ckContainer: container
)
)
return session
}
static func resolveConflict(clientCkRecord: CKRecord, serverCkRecord: CKRecord) -> CKRecord? {
// Implement your own conflict resolution logic
if let clientDate = clientCkRecord.cloudKitLastModifiedDate,
let serverDate = serverCkRecord.cloudKitLastModifiedDate
{
return clientDate > serverDate ? clientCkRecord : serverCkRecord
}
return clientCkRecord
}
static func resolveExpiredChangeToken() -> CKServerChangeToken? {
// Update persisted store to reset the change token to nil
return nil
}
// Listen for fetch work that has been completed
cloudSyncSession.fetchWorkCompletedSubject
.map { _, response in
(response.changeToken, response.changedRecords, response.deletedRecordIDs)
}
.sink { changeToken, ckRecords, recordIDsToDelete in
// Process new and deleted records
if let changeToken = changeToken {
var newChangeTokenData: Data? = try NSKeyedArchiver.archivedData(
withRootObject: changeToken as Any,
requiringSecureCoding: true
)
// Save change token data to disk
}
}
// Listen for modification work that has been completed
cloudSyncSession.modifyWorkCompletedSubject
.map { _, response in
(response.changedRecords, response.deletedRecordIDs, userInfo)
}
.sink { ckRecords, recordIDsToDelete, userInfo in
// Process new and deleted records
}
cloudSyncSession.start()
// Obtain the change token from disk
let changeToken: CKServerChangeToken?
// Queue a fetch operation
cloudSyncSession.fetch(FetchOperation(changeToken: changeToken))
let records: [CKRecord]
let recordIDsToDelete = [CKRecord.ID]
let checkpointID = UUID()
let operation = ModifyOperation(
records: records,
recordIDsToDelete: recordIDsToDelete,
checkpointID: checkpointID,
userInfo: nil
)
cloudSyncSession.modify(operation)
// AppDelegate.swift
func application(_: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if let notification = CKNotification(fromRemoteNotificationDictionary: userInfo),
notification.subscriptionID == Self.subscriptionID {
// Initiate a fetch request with the most recent change token
// Wait some time to see if that fetch operation finishes in time
// Call completionHandler with the appropriate value
return
}
// Handle other kinds of notifications
}
cloudSyncSession.haltedSubject
.sink { error in
// Update UI based on most recent error
}
cloudSyncSession.accountStatusSubject
.sink { accountStatus in
// Update UI with new account status
}
cloudSyncSession.$state
.sink { state in
// Update UI with new sync state
}
cloudSyncSession.eventsPublisher
.sink { [weak self] event in
// Update UI with most recent event
}
要将 CloudSyncSession 与 Swift Package Manager 一起使用,请添加对 https://github.com/ryanashcraft/CloudSyncSession 的依赖。
CloudSyncSession 是基于事件的。事件描述了已发生或已请求的内容。会话的当前状态是截至那一刻之前事件的结果。
CloudSyncSession 使用事件中间件的概念,努力解耦和模块化独立的行为。中间件是有序的;它们可以在事件传递给后续中间件之前转换事件。它们也可以触发副作用。
默认情况下,初始化以下中间件
SplittingMiddleware
:处理拆分大型工作。ErrorMiddleware
:将 CloudKit 错误转换为新事件(例如 retry
、halt
、resolveConflict
等)。RetryMiddleware
:处理如何处理标记为需要重试的工作。WorkMiddleware
:处理将事件转换为对操作处理程序的调用,并根据结果分派新事件。SubjectMiddleware
:将值发送到 CloudSyncSession 实例上的各种 Combine Subject。LoggerMiddleware
:使用 os_log 记录所有事件。ZoneMiddleware
:在会话启动时,分派事件以排队工作来创建区域和关联的订阅。此外,AccountStatusMiddleware
是必需的,但默认情况下未初始化。此中间件在会话启动时检查帐户状态。
CloudSyncSession 状态在不同的“操作模式”之间转换,以确定要处理哪种类型的工作:无(由 nil
表示)、createZone
、createSubscription
、modify
和 fetch
。工作被排队到单独的队列中,每个操作模式一个队列。状态一次仅在一个操作模式下运行。操作模式通过发生的某些事件(例如帐户状态更改和错误)变得符合条件或不符合条件。操作模式是有序的,因此创建区域的工作始终先于修改工作,修改工作同样先于获取工作。
为了使尽可能多的逻辑和行为可测试,大多数 CloudKit 特定代码都通过协议进行了解耦和/或可模拟化。
OperationHandler
是一个协议,它抽象了所有各种操作的处理:FetchOperation
、ModifyOperation
、CreateZoneOperation
和 CreateSubscriptionOperation
。此协议的主要实现 CloudKitOperationHandler
使用标准 CloudKit API 处理这些操作。
有两个测试套件:CloudSyncSessionTests
和 SyncStateTests
。CloudSyncSessionTests
使用自定义 OperationHandler
实例来模拟不同的场景并测试端到端行为,包括重试、拆分工作、处理成功和失败等。
SyncStateTests
断言状态会根据某些事件正确更新。
CloudSyncSession 并非旨在成为将 CloudKit 集成到您的应用程序中的即插即用解决方案。您需要正确地将元数据和记录持久化到磁盘。此外,您必须使用适当的钩子来在您的数据模型与 CKRecord 之间进行转换。
不支持以下 CloudKit 功能
也许这些功能在某种程度上有效,但它们未经测试。如果您对这些功能感兴趣并想验证它们是否有效,请这样做并通过在 GitHub 上提交 issue 来报告您的学习成果。
* 我有意选择不使用引用,因为对于此库设计的用例(在 iCloud 和多个客户端之间镜像数据),它带来的好处有限,但开销却大得多。
此库深受 Cirrus 的影响。此库的部分内容取自 Cirrus 并进行了修改。
我开发 CloudSyncSession 是因为我正在寻找 Cirrus 没有提供的一些 CloudKit 同步库的功能。
这些都是权衡。Cirrus 是一个很棒的库,可能更适合许多应用程序。
基于事件的架构深受 Redux 的影响。
我希望您发现此库对您有所帮助,无论是作为参考还是作为您应用程序的解决方案。为了保持低维护成本并最大限度地降低风险,我没有兴趣进行大型重构或大幅扩展当前提供的功能范围。请随意 fork(参见 LICENSE)。
如果您想提交错误修复或增强功能,请提交 pull request。请包括一些背景信息、您的动机,并在适当的时候添加测试。
请参阅 LICENSE。
此库的部分内容取自 MIT 许可的库 Cirrus 并进行了修改,版权 (c) 2020 Jay Hickey。代码已为此项目的使用进行了修改。