Seam3

CI Status Version License Platform GitHub stars Carthage compatible

Seam3 是一个框架,用于弥合 CoreData 和 CloudKit 之间的差距。它几乎处理了所有 CloudKit 的麻烦。你所要做的就是将其用作 CoreData 存储的存储类型。本地缓存和同步都已处理好。

Seam3 基于 Seam,作者是 nofelmahmood

Seam3 的变更包括

CoreData 到 CloudKit

属性

CoreData CloudKit
NSDate Date/Time (日期/时间)
Binary Data (二进制数据) Bytes 或 CKAsset (见下文)
NSString String (字符串)
Integer16 Int(64)
Integer32 Int(64)
Integer64 Int(64)
Decimal (十进制) Double (双精度浮点数)
Float (单精度浮点数) Double (双精度浮点数)
Boolean (布尔值) Int(64)
NSManagedObject Reference (引用)

在上表中:Integer16Integer32Integer64DecimalFloatBoolean 是指用于在 CoreData 模型中表示它们的 NSNumber 的实例。NSManagedObject 指的是 CoreData 模型中的 `一对一关系`。

如果一个 Binary Data (二进制数据) 属性选择了允许外部存储选项,它将被存储为 CloudKit 中的 CKAsset,否则它将被存储为 Bytes (字节)CKRecord 本身中。

关系

CoreData 关系 CloudKit 上的转换
一对一 一对一关系在 CloudKit 服务器上被转换为 CKReferences。
一对多 不会显式创建一对多关系。Seam3 仅在 CloudKit 服务器上创建和管理一对一关系。
示例 -> 如果一个 Employee (员工) 有一个到 Department (部门) 的一对一关系,并且 Department (部门) 有一个到 Employee (员工) 的一对多关系,那么 Seam3 只会在 CloudKit 服务器上创建前者。它将通过使用一对一关系来实现后者。如果访问一个部门的所有员工,Seam3 将通过获取属于该特定部门的所有员工来实现。

注意:你必须在应用程序的 CoreData 模型中创建反向关系,否则 Seam3 将无法将 CoreData 模型转换为 CloudKit 记录。可能会发生意外错误和数据损坏。

同步

Seam3 使 CoreData 存储与 CloudKit 服务器保持同步。它通过抛出以下两个通知来让你知道同步操作何时开始和结束。

如果在同步操作期间发生错误,则 smSyncDidFinish 通知的 userInfo 属性将包含一个键为 SMStore.SMStoreErrorDomainError (错误) 对象

冲突解决策略

如果发生任何同步冲突,Seam3 会公开 3 种冲突解决策略。

此策略要求你设置 SMStoresyncConflictResolutionBlock 属性。你指定的闭包将接收三个 CKRecord 参数;第一个是当前的服务器记录。第二个是当前的客户端记录,第三个是最近更改之前的客户端记录。你的闭包必须修改并返回作为第一个参数传递的服务器记录。

这是默认值。它将服务器记录视为真实记录。

这会将客户端记录视为真实记录。

如何使用

var smStore: SMStore?
SMStore.registerStoreClass()
do 
{
   self.smStore = try coordinator.addPersistentStoreWithType(SMStore.type, configuration: nil, URL: url, options: nil) as? SMStore
}
lazy var persistentContainer: NSPersistentContainer = {

        SMStore.registerStoreClass()

        let container = NSPersistentContainer(name: "Seam3Demo2")
        
        let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        
        if let applicationDocumentsDirectory = urls.last {
            
            let url = applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
            
            let storeDescription = NSPersistentStoreDescription(url: url)
            
            storeDescription.type = SMStore.type
            
            container.persistentStoreDescriptions=[storeDescription]
            
            container.loadPersistentStores(completionHandler: { (storeDescription, error) in
                if let error = error as NSError? {
                    // Replace this implementation with code to handle the error appropriately.
                    // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                    
                    /*
                     Typical reasons for an error here include:
                     * The parent directory does not exist, cannot be created, or disallows writing.
                     * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                     * The device is out of space.
                     * The store could not be migrated to the current model version.
                     Check the error message to determine what the actual problem was.
                     */
                    fatalError("Unresolved error \(error), \(error.userInfo)")
                }
            })
            return container
        }
        
        fatalError("Unable to access documents directory")
        
    }()

默认情况下,日志将被写入 os_log,但你可以通过扩展 SMLogger 将日志消息路由到你自己的类。

class AppDelegate: SMLogDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        SMStore.logger = self
    }
    // MARK: SMLogDelegate
    func log(_ message: @autoclosure() -> String, type: SMLogType) {
        #if DEBUG
        switch type {
        case .debug:
        print("Debug: \(message())")
        case .info:
        print("Info: \(message())")
        case .error:
        print("Error: \(message())")
        case .fault:
        print("Fault: \(message())")
        case .defaultType:
        print("Default: \(message())")
        }
        #endif
    }
}

你可以使用以下方式访问 SMStore 实例

self.smStore = container.persistentStoreCoordinator.persistentStores.first as? SMStore

在触发同步之前,你应该检查 Cloud Kit 身份验证状态并检查是否有已更改的 Cloud Kit 用户

self.smStore?.verifyCloudKitConnectionAndUser() { (status, user, error) in
    guard status == .available, error == nil else {
        NSLog("Unable to verify CloudKit Connection \(error)")
        return  
    } 

    guard let currentUser = user else {
        NSLog("No current CloudKit user")
        return
    }

    var completeSync = false

    let previousUser = UserDefaults.standard.string(forKey: "CloudKitUser")
    if  previousUser != currentUser {
        do {
            print("New user")
            try self.smStore?.resetBackingStore()
            completeSync = true
        } catch {
            NSLog("Error resetting backing store - \(error.localizedDescription)")
            return
        }
    }

    UserDefaults.standard.set(currentUser, forKey:"CloudKitUser")

    self.smStore?.triggerSync(complete: completeSync)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        print("Received push")

        smStore?.handlePush(userInfo: userInfo) { (result) in
            completionHandler(result.uiBackgroundFetchResult)
        }
    }

跨平台注意事项

默认的 Cloud Kit 容器是使用你的应用程序或应用的bundle identifier来命名的。如果你想在不同平台上的应用程序(例如 iOS 和 macOS)之间共享 Cloud Kit 数据,那么你需要使用命名的 Cloud Kit 容器。你可以在创建 SMStore 实例时指定一个 cloud kit 容器。

在 iOS10 上,使用 NSPersistentStoreDescription 对象指定 SMStore.SMStoreContainerOption

let storeDescription = NSPersistentStoreDescription(url: url)
storeDescription.type = SMStore.type
storeDescription.setOption("iCloud.org.cocoapods.demo.Seam3-Example" as NSString, forKey: SMStore.SMStoreContainerOption)

在 iOS9 和 macOS 上,为持久性存储协调器指定一个选项字典

let options:[String:Any] = [SMStore.SMStoreContainerOption:"iCloud.org.cocoapods.demo.Seam3-Example"]
self.smStore = try coordinator!.addPersistentStore(ofType: SMStore.type, configurationName: nil, at: url, options: options) as? SMStore

确保你在 Xcode 中应用程序的capabilities (功能)选项卡的iCloud containers (iCloud 容器)下选择了你指定的值。

从 Seam 迁移到 Seam3

迁移应该非常简单,因为用于在 CloudKit 和本地后备存储中存储数据的格式没有改变。将 import 语句更改为 import Seam3,你就可以开始了。

示例

要运行示例项目,请克隆 repo,并首先从 Example 目录运行 pod install。如果你在模拟器上运行,请确保使用模拟器中的设置应用登录到 iCloud。

安装

Seam3 可通过 CocoaPods 获得。要安装它,只需将以下行添加到你的 Podfile

pod "Seam3"

作者

paulw, paulw@wilko.me

许可

Seam3 在 MIT 许可下可用。有关更多信息,请参见 LICENSE 文件。