CoreStore 9.2.0

CoreStore 9.2.0

测试测试
Lang语言 SwiftSwift
许可证 MIT
发布上次发布2023年10月
SPM支持 SPM

John Estropia维护。



CoreStore 9.2.0

  • John Rommel Estropia

CoreStore

利用 Swift 的优雅性和安全性释放 Core Data 的真实力量

Build Status Last Commit Platform License

依赖管理器
Cocoapods compatible Carthage compatible Swift Package Manager compatible

联系
Join us on Slack! Reach me on Twitter! Sponsor

从先前的 CoreStore 版本升级?查看 🆕 功能,并确保阅读 变更日志

CoreStore 是 Swift 源兼容性项目 的一部分。

内容

TL;DR (简称示例代码)

纯 Swift 模型

class Person: CoreStoreObject {
    @Field.Stored("name")
    var name: String = ""
    
    @Field.Relationship("pets", inverse: \Dog.$master)
    var pets: Set<Dog>
}

(也支持经典 NSManagedObjects)

使用支持渐进式迁移来设置

dataStack = DataStack(
    xcodeModelName: "MyStore",
    migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"]
)

添加存储

dataStack.addStorage(
    SQLiteStore(fileName: "MyStore.sqlite"),
    completion: { (result) -> Void in
        // ...
    }
)

开始事务

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let person = transaction.create(Into<Person>())
        person.name = "John Smith"
        person.age = 42
    },
    completion: { (result) -> Void in
        switch result {
        case .success: print("success!")
        case .failure(let error): print(error)
        }
    }
)

获取对象(简单)

let people = try dataStack.fetchAll(From<Person>())

获取对象(复杂)

let people = try dataStack.fetchAll(
    From<Person>()
        .where(\.age > 30),
        .orderBy(.ascending(\.name), .descending(.\age)),
        .tweak({ $0.includesPendingChanges = false })
)

查询值

let maxAge = try dataStack.queryValue(
    From<Person>()
        .select(Int.self, .maximum(\.age))
)

但说实话,我编写这份庞大的 README 有其原因。请查看详细内容!

查看 演示 应用项目,获取示例代码!

为什么使用 CoreStore?

CoreStore 最初(且现在)是由开发依赖于数据的实际应用的现实需求所塑造的。它在确保安全且方便地使用 Core Data 的同时,让您能够利用行业推崇的最佳实践。

特性

  • 🆕SwiftUICombine API 工具。强项 ListPublisherObjectPublisher 现在拥有它们的 @ListState@ObjectState SwiftUI 属性包装器。结合 Publisher 也通过 ListPublisher.reactiveObjectPublisher.reactiveDataStack.reactive 命名空间提供。
  • 向后兼容的 DiffableDataSources 实现 UITableViewsUICollectionViews 现在有了新盟友:ListPublisher 提供可比较的快照,使得重新加载动画变得非常容易和安全。告别 UITableViewsUICollectionViews 重新加载错误吧!
  • 💎围绕 Swift 代码优雅性和类型安全性的紧密设计。 CoreStore 完全利用 Swift 的社区驱动语言特性。
  • 🚦更安全的并发架构。 CoreStore使得陷入常见的并发错误变得困难。主要的 NSManagedObjectContext 严格只读,而所有更新都通过串行 事务 完成。 (参见 保存和处理事务
  • 🔍简洁的获取和查询API。 获取对象很容易,但现在查询原始聚合(《min》,《max》等)和原始属性值也同样方便。 (参见 获取和查询
  • 🔭类型安全的,易于配置观察者。 你不必处理设置 NSFetchedResultsController 和 KVO 的负担。额外的好处是,列表和对象观察类型都支持多个观察者。这意味着你可以让多个视图控制器高效地共享单个资源! (参见 观察更改和通知
  • 📥高效的导入工具。 一次用相应的导入来源(例如 JSON)映射实体,从 事务 中导入就变得优雅。唯一化也是通过高效的查找和替换算法完成的。 (参见 导入数据
  • 🗑告别 儿翔ddatamodeld 文件! 尽管 CoreStore 支持 NSManagedObject,但它提供了 CoreStoreObject,其子类可以在 Swift 代码中声明类型安全的属性,而无需维护单独的资源文件。作为额外的好处,这些特殊属性支持自定义类型,并可用于创建类型安全的 keypaths 和查询。 (参见 类型安全的 CoreStoreObject
  • 🔗渐进式迁移。 无需考虑如何从所有以前的模型版本迁移到最新的模型。只需告诉 DataStack 版本字符串(MigrationChain)的顺序,CoreStore 就会在需要时自动使用渐进式迁移。 (参见 迁移
  • 更简单的自定义迁移。 告别 儿xcmappingmodel 文件;CoreStore 现在可以在可能的情况下推断实体映射,同时仍然允许轻松地编写自定义映射。 (参见 迁移
  • 📝插入自己的日志框架。 尽管内置了默认的日志记录器,但所有日志记录、断言和错误报告都可以通过 CoreStoreLogger 协议实现进行。 (参见 日志和错误报告
  • 每个数据堆重持多个持久存储。 CoreStore 允许你在一个 DataStack 中管理多个单独的存储,这与 儿翔dmodelo 配置的设计方式一样。CoreStore 还将默认管理一个堆栈,但你可以根据需要创建和管理尽可能多。 (参见 设置
  • 🎯自由地为实体及其类名命名。 CoreStore 解决了其他 Core Data 包装器中存在的限制,即实体名称应与 NSManagedObject 子类名称相同。CoreStore 从托管对象模型文件加载实体到类的映射,因此你可以为实体及其类名分配独立的名称。
  • 📙完整文档。 没有什么魔法;所有公共类、函数、属性等都有详细的 苹果文档。此 README 还介绍了很多概念,并解释了许多 CoreStore 的行为。
  • ℹ️ 信息性的(并且很漂亮)日志。 所有与 CoreStore 和 Core Data 相关的类型现在都有非常的信息性和漂亮的打印输出! (见 日志和错误报告
  • 🛡 更广泛的单元测试。 扩展 CoreStore 安全,无需担心破坏旧的行为。

有可能会对其他 Core Data 用户有裨益的想法吗? 欢迎提出 功能请求

架构

为了最大程度的安全和性能,CoreStore 将执行它被设计出的编码模式和实际操作。 (别担心,没有听起来那么可怕。)但是在你在你的应用程序中使用 CoreStore 之前理解 CoreStore 的“奥秘”是明智的。

如果你已经熟悉了 CoreData 的内部工作原理,这里有一个 CoreStore 抽象的映射

Core Data CoreStore
NSPersistentContainer
(.xcdatamodeld 文件)
DataStack
NSPersistentStoreDescription
(.xcdatamodeld 文件中的“配置”)
“StorageInterface”实现
(“InMemoryStore”,“SQLiteStore”)
NSManagedObjectContext “BaseDataTransaction”子类
(“SynchronousDataTransaction”、“AsynchronousDataTransaction”、“UnsafeDataTransaction”)

许多 Core Data 封装库都是这样设置它们的“NSManagedObjectContext”的

nested contexts

从子上下文保存到父上下文可以保证上下文间数据的最大完整性,而不会阻塞主队列。但是 实际上,合并上下文通常比保存上下文要快得多。CoreStore 的“DataStack”结合了两者之长,将主上下文作为只读上下文(或“视图上下文”)处理,并且只允许在子上下文上的 事务 中进行更改

nested contexts and merge hybrid

这样可以使主线程更加流畅,同时仍可以利用安全的嵌套上下文。

设置

初始化 CoreStore 最简单的方法是为默认堆栈添加一个默认存储

try CoreStoreDefaults.dataStack.addStorageAndWait()

这一行做了以下操作

  • 触发 CoreStoreDefaults.dataStack 的懒加载初始化,使用默认的 DataStack
  • 设置堆栈的 NSPersistentStoreCoordinator,根保存 NSManagedObjectContext 和只读主 NSManagedObjectContext
  • "Application Support/" 目录中添加一个 SQLiteStore,文件名为 "[App bundle name].sqlite"
  • 在成功时创建并返回 NSPersistentStore 实例,或者在失败时返回 NSError

在大多数情况下,这种配置就足够了。但对于更高端的设置,请参阅这个详尽的示例

let dataStack = DataStack(
    xcodeModelName: "MyModel", // loads from the "MyModel.xcdatamodeld" file
    migrationChain: ["MyStore", "MyStoreV2", "MyStoreV3"] // model versions for progressive migrations
)
let migrationProgress = dataStack.addStorage(
    SQLiteStore(
        fileURL: sqliteFileURL, // set the target file URL for the sqlite file
        configuration: "Config2", // use entities from the "Config2" configuration in the .xcdatamodeld file
        localStorageOptions: .recreateStoreOnModelMismatch // if migration paths cannot be resolved, recreate the sqlite file
    ),
    completion: { (result) -> Void in
        switch result {
        case .success(let storage):
            print("Successfully added sqlite store: \(storage)")
        case .failure(let error):
            print("Failed adding sqlite store with error: \(error)")
        }
    }
)

CoreStoreDefaults.dataStack = dataStack // pass the dataStack to CoreStore for easier access later on

💡如果你从未听说过“配置”,你可以在你的.xcdatamodeld文件中找到它们xcode配置截图

在我们上面的示例代码中,请注意,你不需要做CoreStoreDefaults.dataStack = dataStack这一行。你可以像下面这样直接保存DataStack的引用,并直接调用其所有实例方法

class MyViewController: UIViewController {
    let dataStack = DataStack(xcodeModelName: "MyModel") // keep reference to the stack
    override func viewDidLoad() {
        super.viewDidLoad()
        do {
            try self.dataStack.addStorageAndWait(SQLiteStore.self)
        }
        catch { // ...
        }
    }
    func methodToBeCalledLaterOn() {
        let objects = self.dataStack.fetchAll(From<MyEntity>())
        print(objects)
    }
}

💡默认情况下,CoreStore将从.xcdatamodeld文件初始化NSManagedObject对象,但你可以使用CoreStoreObjectCoreStoreSchema完全从源代码创建模型。要使用此功能,请参阅类型安全的CoreStoreObject

请注意,在我们之前的示例中,addStorageAndWait(_:)addStorage(_:completion:)都接受InMemoryStoreSQLiteStore。这两个实现StorageInterface协议。

内存存储

最基础的StorageInterface具体类型是InMemoryStore,它仅将对象存储在内存中。由于InMemoryStore总是从一个全新的空数据开始,因此它们不需要任何迁移信息。

try dataStack.addStorageAndWait(
    InMemoryStore(
        configuration: "Config2" // optional. Use entities from the "Config2" configuration in the .xcdatamodeld file
    )
)

异步版本

try dataStack.addStorage(
    InMemoryStore(
        configuration: "Config2
    ),
    completion: { storage in
        // ...
    }
)

(关于该方法的响应式编程变体,请参考DataStack组合发布者部分。)

本地存储

你可能会使用最普遍的StorageInterfaceSQLiteStore,它将数据保存在本地的SQLite文件中。

let migrationProgress = dataStack.addStorage(
    SQLiteStore(
        fileName: "MyStore.sqlite",
        configuration: "Config2", // optional. Use entities from the "Config2" configuration in the .xcdatamodeld file
        migrationMappingProviders: [Bundle.main], // optional. The bundles that contain required .xcmappingmodel files
        localStorageOptions: .recreateStoreOnModelMismatch // optional. Provides settings that tells the DataStack how to setup the persistent store
    ),
    completion: { /* ... */ }
)

有关每个默认值的详细说明,请参阅SQLiteStore.swift源代码文档。

CoreStore可以为这些属性决定默认值,因此可以无参数初始化SQLiteStore

try dataStack.addStorageAndWait(SQLiteStore())

(此方法的异步版本将在下一节“迁移”中进一步解释,响应式编程版本在DataStack组合发布者部分中。)

SQLiteStore的相关文件属性实际上是它实现的另一个协议的要求,即LocalStorage协议。

public protocol LocalStorage: StorageInterface {
    var fileURL: NSURL { get }
    var migrationMappingProviders: [SchemaMappingProvider] { get }
    var localStorageOptions: LocalStorageOptions { get }
    func dictionary(forOptions: LocalStorageOptions) -> [String: AnyObject]?
    func cs_eraseStorageAndWait(metadata: [String: Any], soureModelHint: NSManagedObjectModel?) throws
}

如果你有自定义的NSIncrementalStoreNSAtomicStore子类,你可以实现此协议并像SQLiteStore一样使用它。

迁移

声明模型版本

模型版本现以首选协议 DynamicSchema 表达。CoreStore 当前支持以下架构类

  • XcodeDataModelSchema:从 .xcdatamodeld 文件加载实体模型版本。
  • CoreStoreSchema:使用 CoreStoreObject 实体创建的模型版本。(见 类型安全的 CoreStoreObject对象
  • UnsafeDataModelSchema:使用现有的 NSManagedObjectModel 实例创建的模型版本。

所有模型版本的 DynamicSchema 都将收集在单个 SchemaHistory 实例中,然后传递给 DataStack。以下是一些常见用例

.xcdatamodeld 文件中分组的多个模型版本(Core Data 标准方法)

CoreStoreDefaults.dataStack = DataStack(
    xcodeModelName: "MyModel",
    bundle: Bundle.main,
    migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"]
)

CoreStoreSchema 基础模型版本(无需 .xcdatamodeld 文件) (更多信息,请参阅 类型安全的 CoreStoreObject对象

class Animal: CoreStoreObject {
    // ...
}
class Dog: Animal {
    // ...
}
class Person: CoreStoreObject {
    // ...
}

CoreStoreDefaults.dataStack = DataStack(
    CoreStoreSchema(
        modelVersion: "V1",
        entities: [
            Entity<Animal>("Animal", isAbstract: true),
            Entity<Dog>("Dog"),
            Entity<Person>("Person")
        ]
    )
)

过去应用版本中的模型,但已迁移到新的 CoreStoreSchema 方法

class Animal: CoreStoreObject {
    // ...
}
class Dog: Animal {
    // ...
}
class Person: CoreStoreObject {
    // ...
}

let legacySchema = XcodeDataModelSchema.from(
    modelName: "MyModel", // .xcdatamodeld name
    bundle: bundle,
    migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"]
)
let newSchema = CoreStoreSchema(
    modelVersion: "V1",
    entities: [
        Entity<Animal>("Animal", isAbstract: true),
        Entity<Dog>("Dog"),
        Entity<Person>("Person")
    ]
)
CoreStoreDefaults.dataStack = DataStack(
    schemaHistory: SchemaHistory(
        legacySchema + [newSchema],
        migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4", "V1"] 
    )
)   

CoreStoreSchema 基础模型版本带有渐进式迁移

typealias Animal = V2.Animal
typealias Dog = V2.Dog
typealias Person = V2.Person
enum V2 {
    class Animal: CoreStoreObject {
        // ...
    }
    class Dog: Animal {
        // ...
    }
    class Person: CoreStoreObject {
        // ...
    }
}
enum V1 {
    class Animal: CoreStoreObject {
        // ...
    }
    class Dog: Animal {
        // ...
    }
    class Person: CoreStoreObject {
        // ...
    }
}

CoreStoreDefaults.dataStack = DataStack(
    CoreStoreSchema(
        modelVersion: "V1",
        entities: [
            Entity<V1.Animal>("Animal", isAbstract: true),
            Entity<V1.Dog>("Dog"),
            Entity<V1.Person>("Person")
        ]
    ),
    CoreStoreSchema(
        modelVersion: "V2",
        entities: [
            Entity<V2.Animal>("Animal", isAbstract: true),
            Entity<V2.Dog>("Dog"),
            Entity<V2.Person>("Person")
        ]
    ),
    migrationChain: ["V1", "V2"]
)

开始迁移

我们已经看到使用 addStorageAndWait(...) 来初始化我们的持久存储。尽管方法名中的 ~AndWait 后缀暗示了此方法会阻塞,因此它不应执行长时间的任务,如数据迁移。实际上,如果显式提供 .allowSynchronousLightweightMigration 选项,CoreStore 将仅尝试同步 轻量级 迁移

try dataStack.addStorageAndWait(
    SQLiteStore(
        fileURL: sqliteFileURL,
        localStorageOptions: .allowSynchronousLightweightMigration
    )
}

如果这样做,任何模型不匹配将会抛出错误。

一般来说,如果预期需要迁移,建议使用异步变体 addStorage(_:completion:) 方法

let migrationProgress: Progress? = try dataStack.addStorage(
    SQLiteStore(
        fileName: "MyStore.sqlite",
        configuration: "Config2"
    ),
    completion: { (result) -> Void in
        switch result {
        case .success(let storage):
            print("Successfully added sqlite store: \(storage)")
        case .failure(let error):
            print("Failed adding sqlite store with error: \(error)")
        }
    }
)

completion 块报告一个 SetupResult 以指示成功或失败。

(本方法的响应式编程变体在《数据栈合并发布者》章节中有进一步说明)

请注意,此方法还会返回一个可选的进度。如果为nil,则不需要迁移,因此进度报告也不必要。如果不为nil,您可以使用它通过在"fractionCompleted"键上使用标准的KVO或通过使用Progress+Convenience.swift中公开的基于闭包的实用程序来跟踪迁移进度。

migrationProgress?.setProgressHandler { [weak self] (progress) -> Void in
    self?.progressView?.setProgress(Float(progress.fractionCompleted), animated: true)
    self?.percentLabel?.text = progress.localizedDescription // "50% completed"
    self?.stepLabel?.text = progress.localizedAdditionalDescription // "0 of 2"
}

此闭包在主线程上执行,因此可以安全地进行UIKit和AppKit调用。

渐进式迁移

默认情况下,CoreStore使用CoreData的默认自动迁移机制。换句话说,CoreStore将尝试迁移现有的持久化存储,直到它与《SchemaHistory》的currentModelVersion匹配。如果没有从存储的版本找到映射模型路径到数据模型版本,CoreStore将放弃并报告错误。

DataStack允许您通过使用MigrationChain指定如何将迁移拆分为几个子迁移的提示。这通常传递给DataStack初始化器,并将应用于通过addSQLiteStore(...)及其变体添加到DataStack的所有存储。

let dataStack = DataStack(migrationChain: 
    ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"])

最常用的用法是将如上所述按递增顺序传入模型版本(NSManagedObject.xcdatamodeld版本名称或CoreStoreSchemamodelName)。

对于更复杂的非线性迁移路径,您还可以传入映射键值到源-目标版本的版本树。

let dataStack = DataStack(migrationChain: [
    "MyAppModel": "MyAppModelV3",
    "MyAppModelV2": "MyAppModelV4",
    "MyAppModelV3": "MyAppModelV4"
])

这允许根据起始版本有不同的迁移路径。上面的例子将解析为以下路径

  • MyAppModel-MyAppModelV3-MyAppModelV4
  • MyAppModelV2-MyAppModelV4
  • MyAppModelV3-MyAppModelV4

使用空值(无论是nil[]还是[:])初始化告诉DataStack禁用渐进式迁移并恢复到默认迁移行为(即使用.xcdatamodeld的当前版本作为最终版本)

let dataStack = DataStack(migrationChain: nil)

当传递给DataStack时,将验证MigrationChain,如果它不为空,则会在以下条件之一满足时引发断言

  • 数组中出现重复的版本
  • 字典字面量中作为键出现重复的版本
  • 在任何路径中找到循环

⚠️重要:如果指定了MigrationChain,则将绕过.xcdatamodeld的“当前版本”,而MigrationChain的最小版本将成为DataStack的基本模型版本。

预测迁移

有时候迁移过程可能会非常庞大,您可能需要提前获取一些信息,以便您的应用程序能够显示加载界面,或者向用户显示确认对话框。为此,CoreStore提供了一种 requiredMigrationsForStorage(_:) 方法,您可以在实际调用 addStorageAndWait(_:)addStorage(_:completion:) 之前使用该方法检查持久存储。

do {
    let storage = SQLiteStorage(fileName: "MyStore.sqlite")
    let migrationTypes: [MigrationType] = try dataStack.requiredMigrationsForStorage(storage)
    if migrationTypes.count > 1
        || (migrationTypes.filter { $0.isHeavyweightMigration }.count) > 0 {
        // ... will migrate more than once. Show special waiting screen
    }
    else if migrationTypes.count > 0 {
        // ... will migrate just once. Show simple activity indicator
    }
    else {
        // ... Do nothing
    }
    dataStack.addStorage(storage, completion: { /* ... */ })
}
catch {
    // ... either inspection of the store failed, or if no mapping model was found/inferred
}

requiredMigrationsForStorage(_:) 返回一个 MigrationType 的数组,数组中的每个元素可以是以下值之一:

case lightweight(sourceVersion: String, destinationVersion: String)
case heavyweight(sourceVersion: String, destinationVersion: String)

每个 MigrationType 都指示 MigrationChain 中每个步骤的迁移类型。根据您的应用程序需要适当地使用这些信息。

自定义迁移

CoreStore提供了几种声明迁移映射的方法

  • CustomSchemaMappingProvider:一种映射提供程序,最初可以推断映射,但也可以为指定的实体接受自定义映射。这是为了支持使用 CoreStoreObject 的自定义迁移,但也可以与 NSManagedObject 一起使用。
  • XcodeSchemaMappingProvider:一种映射提供程序,它从指定 Bundle 中的 .xcmappingmodel 文件中加载实体映射。
  • InferredSchemaMappingProvider:默认映射提供程序,尝试在两个 DynamicSchema 版本之间推断模型迁移,通过搜索 Bundle.allBundles 中的所有 .xcmappingmodel 文件来实现,或者在可能的情况下依赖轻量级迁移。

这些映射提供程序符合 SchemaMappingProvider 协议,可以传递给 SQLiteStore 的初始化器。

let dataStack = DataStack(migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"])
_ = try dataStack.addStorage(
    SQLiteStore(
        fileName: "MyStore.sqlite",
        migrationMappingProviders: [
            XcodeSchemaMappingProvider(from: "V1", to: "V2", mappingModelBundle: Bundle.main),
            CustomSchemaMappingProvider(from: "V2", to: "V3", entityMappings: [.deleteEntity("Person") ])
        ]
    ),
    completion: { (result) -> Void in
        // ...
    }
)

对于 DataStackMigrationChain 中存在但未由 SQLiteStoremigrationMappingProviders 数组中的任何提供程序处理的版本迁移,CoreStore 会自动尝试使用 InferredSchemaMappingProvider 作为后备方案。最后,如果 InferredSchemaMappingProvider 无法解决任何映射,迁移将失败,并且 DataStack.addStorage(...) 方法将报告失败。

对于 CustomSchemaMappingProvider,通过动态对象 UnsafeSourceObjectUnsafeDestinationObject 支持更细粒度的更新。以下示例允许迁移有条件地忽略一些对象。

let person_v2_to_v3_mapping = CustomSchemaMappingProvider(
    from: "V2",
    to: "V3",
    entityMappings: [
        .transformEntity(
            sourceEntity: "Person",
            destinationEntity: "Person",
            transformer: { (sourceObject: UnsafeSourceObject, createDestinationObject: () -> UnsafeDestinationObject) in
                
                if (sourceObject["isVeryOldAccount"] as! Bool?) == true {
                    return // this account is too old, don't migrate 
                }
                // migrate the rest
                let destinationObject = createDestinationObject()
                destinationObject.enumerateAttributes { (attribute, sourceAttribute) in
                
                if let sourceAttribute = sourceAttribute {
                    destinationObject[attribute] = sourceObject[sourceAttribute]
                }
            }
        ) 
    ]
)
SQLiteStore(
    fileName: "MyStore.sqlite",
    migrationMappingProviders: [person_v2_to_v3_mapping]
)

UnsafeSourceObject 是对源模型版本中现有对象的只读代理。而 UnsafeDestinationObject 是一个可读写的对象,可以选择性地插入到目标模型版本中。这两个类的属性都通过键值编码访问。

保存和处理事务

为确保只读 NSManagedObjectContext 中对象状态的确定性,CoreStore 不公开直接从主上下文(或任何其他上下文)更新和保存的 API。相反,您从 DataStack 实例生成 事务

let dataStack = self.dataStack
dataStack.perform(
    asynchronous: { (transaction) -> Void in
        // make changes
    },
    completion: { (result) -> Void in
        // ...
    }
)

事务闭包在闭包完成时会自动保存更改。要取消和回滚事务,在闭包内部抛出 CoreStoreError.userCancelled,并通过调用 try transaction.cancel() 实现。

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        // ...
        if shouldCancel {
            try transaction.cancel()
        }
        // ...
    },
    completion: { (result) -> Void in
        if case .failure(.userCancelled) = result {
            // ... cancelled
        }
    }
)

⚠️重要提示:绝对不要在 transaction.cancel() 调用上使用 try?try!。始终使用 try。使用 try? 将吞没取消,事务将正常继续保存。使用 try! 将导致应用程序崩溃,因为 transaction.cancel() 将始终引发错误。

上面的示例使用了 perform(asynchronous:...),但实际上有三种类型的事务可供您使用:异步同步不安全

事务类型

异步事务

perform(asynchronous:...) 生成。该方法立即返回并从后台串行队列上执行其闭包。闭包的返回值声明为一个泛型类型,因此可以从闭包返回的任何值都可以传递到完成结果中。

dataStack.perform(
    asynchronous: { (transaction) -> Bool in
        // make changes
        return transaction.hasChanges
    },
    completion: { (result) -> Void in
        switch result {
        case .success(let hasChanges): print("success! Has changes? \(hasChanges)")
        case .failure(let error): print(error)
        }
    }
)

成功和失败也可以声明为单独的处理程序。

dataStack.perform(
    asynchronous: { (transaction) -> Int in
        // make changes
        return transaction.delete(objects)
    },
    success: { (numberOfDeletedObjects: Int) -> Void in
        print("success! Deleted \(numberOfDeletedObjects) objects")
    },
    failure: { (error) -> Void in
        print(error)
    }
)

⚠️在事务闭包中返回 NSManagedObjectCoreStoreObject 时请小心。这些实例仅用于事务本身。请参阅安全传递对象

perform(asynchronous:...) 创建的事务是 AsynchronousDataTransaction 的实例。

同步事务

perform(synchronous:...) 创建。虽然语法与其异步对应项类似,但 perform(synchronous:...) 在返回之前会等待其事务代码块完成。

let hasChanges = dataStack.perform(
    synchronous: { (transaction) -> Bool in
        // make changes
        return transaction.hasChanges
    }
)

transaction 上述是 SynchronousDataTransaction 实例。

由于perform(synchronous:...)实际上会阻塞两个队列(调用者的队列和事务的后台队列),因此它被认为安全性较低,更容易发生死锁。请注意,闭包不应在任何其他外部队列上阻塞。

默认情况下,perform(synchronous:...)将在方法返回之前等待观察者,如ListMonitor被通知。这可能会导致死锁,尤其是如果您在这个主线程中调用它。为了降低这种风险,您可能尝试将waitForAllObservers:参数设置为false。这样做会告诉SynchronousDataTransaction只在完成保存时阻塞,不会等待其他上下文接收这些更改。这降低了死锁风险,但可能会有意外的副作用

dataStack.perform(
    synchronous: { (transaction) in
        let person = transaction.create(Into<Person>())
        person.name = "John"
    },
    waitForAllObservers: false
)
let newPerson = dataStack.fetchOne(From<Person>.where(\.name == "John"))
// newPerson may be nil!
// The DataStack may have not yet received the update notification.

由于同步事务的复杂性质,如果您应用程序的事务吞吐量非常高,强烈建议您改用异步事务

不安全的交易

特别之处在于它们不会在闭包内部包含更新

let transaction = dataStack.beginUnsafe()
// make changes
downloadJSONWithCompletion({ (json) -> Void in

    // make other changes
    transaction.commit()
})
downloadAnotherJSONWithCompletion({ (json) -> Void in

    // make some other changes
    transaction.commit()
})

这允许进行不连续的更新。请注意,这种灵活性是有代价的:您现在必须负责管理事务的并发。正如本叔叔所说的,“权力越大,越容易产生竞争条件”。

如上例所示,对于不安全的交易,可以多次调用commit()

您已经了解了如何创建事务,但尚未了解如何进行创建更新删除操作。上面提到的3种类型的事务都是BaseDataTransaction的子类,它实现了以下方法。

创建对象

create(...)方法接受一个Into子句,指定您要创建的对象的实体

let person = transaction.create(Into<MyPersonEntity>())

虽然语法很简单,但CoreStore不会天真地插入一个新的对象。这一行做了以下事情

  • 检查实体类型是否存在于事务的任何父持久存储中
  • 如果实体只属于一个持久存储,则将新对象插入该存储,并从create(...)返回
  • 如果实体不属于任何存储,则引发断言失败。这是程序员错误,在生产代码中不应发生。
  • 如果实体属于多个商店,将引发断言失败。这同样是一个编程错误,在生产代码中绝不应发生。 通常情况下,使用 Core Data 可以在当前状态插入对象,但保存 NSManagedObjectContext 恒将失败。CoreStore 在创建时间检查这一点(不是在保存时)。

如果实体存在于多个配置中,您需要为目的地持久存储提供配置名称

let person = transaction.create(Into<MyPersonEntity>("Config1"))

或者,如果持久存储是自动生成的“默认”配置,指定 nil

let person = transaction.create(Into<MyPersonEntity>(nil))

请注意,如果您明确指定了配置名称,CoreStore 将只尝试将创建的对象插入到该特定存储中,如果找不到该存储,则失败;它不会回退到实体所属的任何其他配置。

更新对象

从事务中创建对象后,您可以像平常一样更新其属性

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let person = transaction.create(Into<MyPersonEntity>())
        person.name = "John Smith"
        person.age = 30
    },
    completion: { _ in }
)

要更新现有对象,从事务中检索对象的实例

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let person = try transaction.fetchOne(
            From<MyPersonEntity>()
                .where(\.name == "Jane Smith")
        )
        person.age = person.age + 1
    },
    completion: { _ in }
)

(有关检索的更多信息,请参阅 检索和查询)

不要更新未从事务中创建/检索的实例。 如果您已经有了对象的引用,请使用事务的 edit(...) 方法来获取对象的可编辑代理实例

let jane: MyPersonEntity = // ...

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        // WRONG: jane.age = jane.age + 1
        // RIGHT:
        let jane = transaction.edit(jane)! // using the same variable name protects us from misusing the non-transaction instance
        jane.age = jane.age + 1
    },
    completion: { _ in }
)

在更新对象的关联时也是如此。确保分配给关联的对象也是从事务中创建/检索的

let jane: MyPersonEntity = // ...
let john: MyPersonEntity = // ...

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        // WRONG: jane.friends = [john]
        // RIGHT:
        let jane = transaction.edit(jane)!
        let john = transaction.edit(john)!
        jane.friends = NSSet(array: [john])
    },
    completion: { _ in }
)

删除对象

删除对象更为简单,因为您可以告诉事务直接删除对象而无需检索可编辑代理(CoreStore 会为您完成此操作)

let john: MyPersonEntity = // ...

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        transaction.delete(john)
    },
    completion: { _ in }
)

或者同时删除多个对象

let john: MyPersonEntity = // ...
let jane: MyPersonEntity = // ...

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        try transaction.delete(john, jane)
        // try transaction.delete([john, jane]) is also allowed
    },
    completion: { _ in }
)

如果您还没有要删除的对象的引用,事务提供了一个可以传递查询的 deleteAll(...) 方法

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        try transaction.deleteAll(
            From<MyPersonEntity>()
                .where(\.age > 30)
        )
    },
    completion: { _ in }
)

安全传递对象

始终记得,DataStack和单个事务管理不同的NSManagedObjectContext,所以您不能在它们之间直接使用对象。这就是为什么事务有edit(...)方法。

let jane: MyPersonEntity = // ...

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let jane = transaction.edit(jane)!
        jane.age = jane.age + 1
    },
    completion: { _ in }
)

CoreStoreDataStackBaseDataTransaction具有非常灵活的fetchExisting(...)方法,您可以使用该方法在实例之间互相传递。

let jane: MyPersonEntity = // ...

dataStack.perform(
    asynchronous: { (transaction) -> MyPersonEntity in
        let jane = transaction.fetchExisting(jane)! // instance for transaction
        jane.age = jane.age + 1
        return jane
    },
    success: { (transactionJane) in
        let jane = dataStack.fetchExisting(transactionJane)! // instance for DataStack
        print(jane.age)
    },
    failure: { (error) in
        // ...
    }
)

fetchExisting(...)还适用于多个NSManagedObjectCoreStoreObjectNSManagedObjectID

var peopleIDs: [NSManagedObjectID] = // ...

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let jane = try transaction.fetchOne(
            From<MyPersonEntity>()
                .where(\.name == "Jane Smith")
        )
        jane.friends = NSSet(array: transaction.fetchExisting(peopleIDs)!)
        // ...
    },
    completion: { _ in }
)

导入数据

有时,如果不是大多数时间,我们保存到Core Data的数据来自外部源,例如Web服务器或外部文件。例如,如果您有个JSON字典,您可能这样提取值:

let json: [String: Any] = // ...
person.name = json["name"] as? NSString
person.age = json["age"] as? NSNumber
// ...

如果您有许多属性,您不希望在每次导入数据时都重复映射。CoreStore允许您只写一次数据映射代码,您只需要通过BaseDataTransaction子类中的importObject(...)importUniqueObject(...)调用即可。

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let json: [String: Any] = // ...
        try! transaction.importObject(
            Into<MyPersonEntity>(),
            source: json
        )
    },
    completion: { _ in }
)

为了支持实体的数据导入,请在NSManagedObjectCoreStoreObject子类上实现ImportableObjectImportableUniqueObject

  • ImportableObject:如果您希望对象没有固有唯一性,并且每次调用importObject(...)时都始终添加新对象,请使用此协议。
  • ImportableUniqueObject:要为对象指定唯一ID,以用于在调用importUniqueObject(...)时区分是否创建新对象或更新现有对象,请使用此协议。

这两个协议都要求实现者指定一个ImportSource,它可以设置为对象可以从中提取数据的任何类型。

typealias ImportSource = NSDictionary
typealias ImportSource = [String: Any]
typealias ImportSource = NSData

您甚至可以使用来自流行的第三方JSON库的外部类型,或者使用简单的元组或原始数据类型。

ImportableObject

ImportableObject 是一个非常简单的协议

public protocol ImportableObject: AnyObject {
    typealias ImportSource
    static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
    func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws
}

首先,将 ImportSource 设置为数据源预期的类型

typealias ImportSource = [String: Any]

这样我们就可以用任意 `[String: Any]` 类型的参数调用 importObject(_:source:)

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let json: [String: Any] = // ...
        try! transaction.importObject(
            Into<MyPersonEntity>(),
            source: json
        )
        // ...
    },
    completion: { _ in }
)

实际提取和赋值应在 ImportableObject 协议的 didInsert(from:in:) 方法中实现

func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws {
    self.name = source["name"] as? NSString
    self.age = source["age"] as? NSNumber
    // ...
}

事务还允许您使用 importObjects(_:sourceArray:) 方法一次性导入多个对象

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let jsonArray: [[String: Any]] = // ...
        try! transaction.importObjects(
            Into<MyPersonEntity>(),
            sourceArray: jsonArray // make sure this is of type Array<MyPersonEntity.ImportSource>
        )
        // ...
    },
    completion: { _ in }
)

这样做会使事务通过迭代导入源数组并调用 ImportableObjectshouldInsert(from:in:) 来确定哪些实例应该被创建。如果你想跳过一个源并继续处理数组中的其他源,你可以在 shouldInsert(from:in:) 中返回 false

另一方面,如果你的验证失败导致其他所有源也应该回滚和取消,你可以在 didInsert(from:in:) 中抛出异常

func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws {
    self.name = source["name"] as? NSString
    self.age = source["age"] as? NSNumber
    // ...
    if self.name == nil {
        throw Errors.InvalidNameError
    }
}

这样你可以立即放弃无效的事务

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let jsonArray: [[String: Any]] = // ...

        try transaction.importObjects(
            Into<MyPersonEntity>(),
            sourceArray: jsonArray
        )
    },
    success: {
        // ...
    },
    failure: { (error) in
        switch error {
        case Errors.InvalidNameError: print("Invalid name")
        // ...
        }
    }
)

ImportableUniqueObject

通常,我们不仅每次导入数据时都会创建对象。通常我们还需要更新已存在的对象。实现 ImportableUniqueObject 协议允许您指定一个“唯一 ID”,事务可以用来在创建新对象之前搜索现有对象

public protocol ImportableUniqueObject: ImportableObject {
    typealias ImportSource
    typealias UniqueIDType: ImportableAttributeType

    static var uniqueIDKeyPath: String { get }
    var uniqueIDValue: UniqueIDType { get set }

    static func shouldInsert(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
    static func shouldUpdate(from source: ImportSource, in transaction: BaseDataTransaction) -> Bool
    static func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> UniqueIDType?
    func didInsert(from source: ImportSource, in transaction: BaseDataTransaction) throws
    func update(from source: ImportSource, in transaction: BaseDataTransaction) throws
}

请注意,它与 ImportableObject 有相同的插入方法,增加了更新方法和指定唯一 ID 的方法

class var uniqueIDKeyPath: String {
    return #keyPath(MyPersonEntity.personID) 
}
var uniqueIDValue: Int { 
    get { return self.personID }
    set { self.personID = newValue }
}
class func uniqueID(from source: ImportSource, in transaction: BaseDataTransaction) throws -> Int? {
    return source["id"] as? Int
}

对于 ImportableUniqueObject,值的提取和赋值应从 update(from:in:) 方法实现。默认情况下,didInsert(from:in:) 会调用 update(from:in:),但如果需要,你可以分别实现插入和更新的实现。

然后,你可以通过调用事务的 importUniqueObject(...) 方法来创建/更新一个对象

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let json: [String: Any] = // ...
        try! transaction.importUniqueObject(
            Into<MyPersonEntity>(),
            source: json
        )
        // ...
    },
    completion: { _ in }
)

或者使用 importUniqueObjects(...) 方法一次性创建/更新多个对象

dataStack.perform(
    asynchronous: { (transaction) -> Void in
        let jsonArray: [[String: AnyObject]] = // ...
        try! transaction.importUniqueObjects(
            Into<MyPersonEntity>(),
            sourceArray: jsonArray
        )
        // ...
    },
    completion: { _ in }
)

ImportableObject 类似,你可以通过实现 shouldInsert(from:in:)shouldUpdate(from:in:) 来控制是否跳过一个对象,或者通过在 uniqueID(from:in:)didInsert(from:in:)update(from:in:) 方法中抛出错误来取消所有对象。

获取和查询

在我们深入探讨之前,请注意CoreStore将获取查询区分开来。

  • 一个获取从特定的事务数据栈执行搜索。这意味着获取可以包括挂起的对象(例如,在事务调用commit()之前。)。当需要以下情况时使用获取:
    • 结果需要是NSManagedObjectCoreStoreObject实例
    • 在搜索中需要包含未保存的对象(尽管可以配置获取以排除它们)
  • 查询直接从持久存储中提取数据。这意味着当计算汇总(例如,计数最小值最大值等)时搜索更快。当需要以下情况时使用查询:
    • 需要计算汇总函数(以下将列出支持的函数列表)
    • 结果可以是原始值,如NSStringNSNumberIntNSDateNSDictionary的键值对或任何符合QueryableAttributeType的类型。(请参阅QueryableAttributeType.swift以获取内置类型的列表)
    • 只有在结果中需要包含指定的属性键值
    • 忽略未保存的对象

From子句

使用子句指定获取和查询的搜索条件。所有获取和查询都需要一个表示目标实体类型的From子句。

let people = try dataStack.fetchAll(From<MyPersonEntity>())

上面的示例中的people将是类型[MyPersonEntity]From<MyPersonEntity>()子句表示向所有属于MyPersonEntity的持久存储的获取。

如果实体存在于多个配置中,并且您只需要从特定的配置中进行搜索,则请在From子句中指定目标持久存储的配置名称。

let people = try dataStack.fetchAll(From<MyPersonEntity>("Config1")) // ignore objects in persistent stores other than the "Config1" configuration

或者,如果持久存储是自动生成的“默认”配置,指定 nil

let person = try dataStack.fetchAll(From<MyPersonEntity>(nil))

现在我们已经知道了如何使用From子句,接下来让我们看看如何获取和查询。

获取

当前从 CoreStoreDataStack 实例或 BaseDataTransaction 实例中,您可以调用 5 种获取方法。以下所有方法都接受相同的参数:一个必需的 From 子句,以及一系列可选的 WhereOrderBy 和/或 Tweak 子句。

  • fetchAll(...) - 返回与条件匹配的所有对象的数组。
  • fetchOne(...) - 返回与条件匹配的第一个对象。
  • fetchCount(...) - 返回与条件匹配的对象数量。
  • fetchObjectIDs(...) - 返回与条件匹配的所有对象的 NSManagedObjectID 数组。
  • fetchObjectID(...) - 返回与条件匹配的第一个对象的 NSManagedObjectID

每个方法的目的都很简单,但我们需要了解如何设置获取的子句。

Where 子句

Where 子句是 CoreStore 的 NSPredicate 包装器。它指定在获取(或查询)时使用的搜索过滤器。它实现了 NSPredicate 所有的初始化器(除不支持 -predicateWithBlock: 外,这是 Core Data 不支持的)。

var people = try dataStack.fetchAll(
    From<MyPersonEntity>(),
    Where<MyPersonEntity>("%K > %d", "age", 30) // string format initializer
)
people = try dataStack.fetchAll(
    From<MyPersonEntity>(),
    Where<MyPersonEntity>(true) // boolean initializer
)

如果您已经有了现有的 NSPredicate 实例,也可以将其传递给 Where

let predicate = NSPredicate(...)
var people = dataStack.fetchAll(
    From<MyPersonEntity>(),
    Where<MyPersonEntity>(predicate) // predicate initializer
)

Where 子句是泛型类型。为了防止泛型对象类型的冗长重复,获取方法支持 Fetch Chain 构建器。我们还可以使用 Swift 的智能 KeyPaths 作为 Where 子句表达式。

var people = try dataStack.fetchAll(
    From<MyPersonEntity>()
        .where(\.age > 30) // Type-safe!
)

Where 子句还实现了 &&||! 逻辑运算符,因此您可以在不写出太多的 ANDORNOT 字符串的情况下提供逻辑条件。

var people = try dataStack.fetchAll(
    From<MyPersonEntity>()
        .where(\.age > 30 && \.gender == "M")
)

如果您没有提供 Where 子句,则将返回属于指定 From 的所有对象。

OrderBy 子句

OrderBy 子句是 CoreStore 的 NSSortDescriptor 包装器。使用它来指定要按其排序获取(或查询)结果的属性键。

var mostValuablePeople = try dataStack.fetchAll(
    From<MyPersonEntity>(),
    OrderBy<MyPersonEntity>(.descending("rating"), .ascending("surname"))
)

如上所示,OrderBy 接受一个 SortKey 枚举值的列表,可以是 .ascending.descending。与 Where 子句一样,OrderBy 子句也是泛型类型。为了避免泛型对象类型的冗长重复,获取方法支持 Fetch Chain 构建器。我们还可以使用 Swift 的智能 KeyPaths 作为 OrderBy 子句表达式。

var people = try dataStack.fetchAll(
    From<MyPersonEntity>()
        .orderBy(.descending(\.rating), .ascending(\.surname)) // Type-safe!
)

您可以使用 ++= 运算符将 OrderBy 连接起来。这在条件排序时很有用。

var orderBy = OrderBy<MyPersonEntity>(.descending(\.rating))
if sortFromYoungest {
    orderBy += OrderBy(.ascending(\.age))
}
var mostValuablePeople = try dataStack.fetchAll(
    From<MyPersonEntity>(),
    orderBy
)

调整语句

调整语句允许您,呃,调整获取(或查询)。调整NSFetchRequest暴露在闭包中,您可以在其中修改其属性

var people = try dataStack.fetchAll(
    From<MyPersonEntity>(),
    Where<MyPersonEntity>("age > %d", 30),
    OrderBy<MyPersonEntity>(.ascending("surname")),
    Tweak { (fetchRequest) -> Void in
        fetchRequest.includesPendingChanges = false
        fetchRequest.returnsObjectsAsFaults = false
        fetchRequest.includesSubentities = false
    }
)

调整还支持<强>获取链构建器

var people = try dataStack.fetchAll(
    From<MyPersonEntity>(),
        .where(\.age > 30)
        .orderBy(.ascending(\.surname))
        .tweak {
            $0.includesPendingChanges = false
            $0.returnsObjectsAsFaults = false
            $0.includesSubentities = false
        }
)

子句按其在获取/查询中出现的顺序评估,因此您通常需要将调整设置为最后一个子句。调整的闭包将在获取发生之前执行,因此请确保闭包捕获的任何值不会引起竞争条件。

虽然调整允许您微调NSFetchRequest,但请注意,CoreStore已经预先配置了相应的NSFetchRequest以适合的默认值。只有当您知道自己在做什么时才使用调整

查询

其他Core Data包装库忽视的功能之一是原始属性获取。如果您熟悉NSDictionaryResultType-[NSFetchedRequest propertiesToFetch],您可能知道设置查询原始值和聚合值有多么痛苦。CoreStore通过公开以下两个方法使这变得容易

  • queryValue(...) - 返回一个属性或聚合值的单个原始值。如果有多个结果,queryValue(...)只返回第一个项目。
  • queryAttributes(...) - 返回一个包含属性键及其对应值的字典数组。

上述两个方法接受相同的参数:一个必需的From子句,一个必需的Select<T>子句,以及可选的一系列WhereOrderByGroupBy和/或Tweak子句。

设置FromWhereOrderByTweak子句的方式与获取类似。对于查询,您还需要了解如何使用Select<T>GroupBy子句。

Select<T>子句

Select<T>子句指定目标属性/聚合键以及预期返回类型

let johnsAge = try dataStack.queryValue(
    From<MyPersonEntity>(),
    Select<Int>("age"),
    Where<MyPersonEntity>("name == %@", "John Smith")
)

上述示例用于查询第一个符合Where条件的对象的“age”属性。由于使用了指示的Select泛型类型,因此johnsAge将绑定到类型Int?。对于queryValue(...),允许将符合QueryableAttributeType的类型作为返回类型(因此也作为Select的泛型类型)。

对于queryAttributes(...),只有NSDictionary是有效的Select类型,因此您可以省略泛型类型。

let allAges = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("age")
)

查询方法也支持查询链构建器。我们还可以使用Swift的智能键路径(Smart KeyPaths)在表达式中使用。

let johnsAge = try dataStack.queryValue(
    From<MyPersonEntity>()
        .select(\.age) // binds the result to Int
        .where(\.name == "John Smith")
)

如果您只需特定属性的值,只需指定键名(例如,我们使用Select("age")做的那样),但也可以使用几个聚合函数作为Select的参数。

  • .average(...)
  • .count(...)
  • .maximum(...)
  • .minimum(...)
  • .sum(...)
let oldestAge = try dataStack.queryValue(
    From<MyPersonEntity>(),
    Select<Int>(.maximum("age"))
)

对于返回字典数组的queryAttributes(...),您可以在Select中指定多个属性/聚合函数。

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("name", "age")
)

personJSON将具有以下值

[
    [
        "name": "John Smith",
        "age": 30
    ],
    [
        "name": "Jane Doe",
        "age": 22
    ]
]

您还可以包括一个聚合函数

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("name", .count("friends"))
)

它返回

[
    [
        "name": "John Smith",
        "count(friends)": 42
    ],
    [
        "name": "Jane Doe",
        "count(friends)": 231
    ]
]

CoreStore自动使用“count(friends)”作为键名,但如果您需要,可以指定自己的键别名

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("name", .count("friends", as: "friendsCount"))
)

它现在返回

[
    [
        "name": "John Smith",
        "friendsCount": 42
    ],
    [
        "name": "Jane Doe",
        "friendsCount": 231
    ]
]

GroupBy 子句

GroupBy 子句允许您按指定的属性/聚合函数分组结果。这只对queryAttributes(...)有用,因为queryValue(...)只返回第一个值。

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>(),
    Select("age", .count("age", as: "count")),
    GroupBy("age")
)

GroupBy 子句也是泛型类型,并支持查询链构建器。我们还可以使用Swift的智能键路径(Smart KeyPaths)在表达式中使用。

let personJSON = try dataStack.queryAttributes(
    From<MyPersonEntity>()
        .select(.attribute(\.age), .count(\.age, as: "count"))
        .groupBy(\.age)
)

这返回显示每个“age”的计数的字典

[
    [
        "age": 42,
        "count": 1
    ],
    [
        "age": 22,
        "count": 1
    ]
]

日志和错误报告

在使用一些第三方库时,它们通常会用自己的日志机制污染控制台。CoreStore提供了一个默认的日志类,但您可以通过实现CoreStoreLogger协议来插入您自己的最喜欢日志工具。

public protocol CoreStoreLogger {
    func log(level level: LogLevel, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
    func log(error error: CoreStoreError, message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
    func assert(@autoclosure condition: () -> Bool, @autoclosure message: () -> String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
    func abort(message: String, fileName: StaticString, lineNumber: Int, functionName: StaticString)
}

用您的自定义类实现此协议,然后将其实例传递给CoreStoreDefaults.logger

CoreStoreDefaults.logger = MyLogger()

这样做将所有日志调用通道到您的日志记录器。

请注意,为了保留调用堆栈信息,对上述方法的调用**不**受线程管理。因此,您必须确保您的日志记录器是线程安全的,或者您可能必须将日志实现调度到一个串行队列中。

在实现 CoreStoreLoggerassert(...)abort(...) 函数时,请特别小心

  • assert(...):在 DEBUG 和发布版本、或 -O-Onone 之间的行为,均留给实现者自行负责。CoreStore 只调用 CoreStoreLogger.assert(...) 处理无效但通常可恢复的错误(例如,较早的验证失败可能导致在别处抛出和处理的错误)
  • abort(...):此方法是应用在 CoreStore 中同步记录一个致命错误的最后一个机会。在该函数被调用后,应用将被终止(CoreStore 内部调用 fatalError()

所有 CoreStore 类型都有非常实用的(格式也很美观的!)print(...) 输出。以下是一些示例,如 ListMonitor

screen shot 2016-07-10 at 22 56 44

CoreStoreError.mappingModelNotFoundError:

MappingModelNotFoundError

这些都是通过 CustomDebugStringConvertible.debugDescription 实现的,因此它们也可以和 lldb 的 po 命令一起使用。

监视更改和通知

CoreStore 为监视托管对象提供了类型安全的包装器

🆕ObjectPublisher ObjectMonitor 🆕ListPublisher ListMonitor
对象数量 1 1 N N
允许多个观察者
发出细粒度更改
发出支持差异数据的快照
委托方法
闭包回调
支持 SwiftUI

监视单个属性

为了得到对象中单个属性更改的通知,根据对象的基础类有两种方法。

  • 对于 NSManagedObject 子类:使用标准的 KVO(键值观察)方法
let observer = person.observe(\.age, options: [.new]) { (person, change)
    print("Happy \(change.newValue)th birthday!")
}
  • 对于 CoreStoreObject 子类:在属性上直接调用 observe(...) 方法。你会注意到 API 本身与 KVO 方法有点相似
let observer = person.age.observe(options: [.new]) { (person, change)
    print("Happy \(change.newValue)th birthday!")
}

对于这两种方法,你都需要在整个观察期间保持对返回的 observer 的引用。

观察单个对象的更新

ObjectPublisher的观察者可以在对象的任何属性变化时收到通知。您可以直接从对象创建一个ObjectPublisher

let objectPublisher: ObjectPublisher<Person> = person.asPublisher(in: dataStack)

或从listPublisherListSnapshot索引

let listPublisher: ListPublisher<Person> = // ...
// ...
let objectPublisher = listPublisher.snapshot[indexPath]

(请参阅下文ListPublisher示例)

要接收通知,请调用ObjectPublisheraddObserve(...)方法并传递回调闭包的所有者

objectPublisher.addObserver(self) { [weak self] (objectPublisher) in
    let snapshot: ObjectSnapshot<Person> = objectPublisher.snapshot
    // handle changes
}

请注意,所有者实例将不会被保留。您可以通过显式调用ObjectPublisher.removeObserver(...)来停止接收通知,但是ObjectPublisher也会停止向已释放的观察者发送事件。

ObjectPublisher.snapshot属性返回的ObjectSnapshot返回对象所有属性的全拷贝struct。这对于管理状态是理想的,因为它们是线程安全的,并且不会受到实际对象进一步更改的影响。ObjectPublisher会自动将其snapshot值更新为对象的最新状态。

(关于此方法的响应式编程变种,请参阅ObjectPublisher组合发布者的ObjectPublisher部分)

观察单个对象每个属性的更新

如果您需要跟踪对象中哪些属性发生了变化,则需实现ObjectObserver协议并指定EntityType

class MyViewController: UIViewController, ObjectObserver {
    func objectMonitor(monitor: ObjectMonitor<MyPersonEntity>, willUpdateObject object: MyPersonEntity) {
        // ...
    }
    
    func objectMonitor(monitor: ObjectMonitor<MyPersonEntity>, didUpdateObject object: MyPersonEntity, changedPersistentKeys: Set<KeyPathString>) {
        // ...
    }
    
    func objectMonitor(monitor: ObjectMonitor<MyPersonEntity>, didDeleteObject object: MyPersonEntity) {
        // ...
    }
}

然后我们需要保留一个ObjectMonitor实例,并注册我们的ObjectObserver作为观察者

let person: MyPersonEntity = // ...
self.monitor = dataStack.monitorObject(person)
self.monitor.addObserver(self)

控制器将在对象属性更改时通知我们的观察者。您可以将多个ObjectObserver添加到单个ObjectMonitor中而不会有任何问题。这意味着您可以无问题地将ObjectMonitor实例在多个屏幕之间共享。

您可以通过它的object属性获取ObjectMonitor的对象。如果对象被删除,则object属性将变为nil以防止进一步访问。

虽然ObjectMonitor也公开了removeObserver(...),但它只存储观察者的weak引用,并会安全地注销已释放的观察者。

观察可区分列表

ListPublisher 的获取结果集发生变化时,其观察者可以接收到通知。您可以通过从 DataStack 获取数据来创建

let listPublisher = dataStack.listPublisher(
    From<Person>()
        .sectionBy(\.age") { "Age \($0)" } // sections are optional
        .where(\.title == "Engineer")
        .orderBy(.ascending(\.lastName))
)

要接收通知,请调用 addObserve(...) 方法,传入回调闭包的所有者

listPublisher.addObserver(self) { [weak self] (listPublisher) in
    let snapshot: ListSnapshot<Person> = listPublisher.snapshot
    // handle changes
}

请注意,所有者实例将不会被保留。您可以通过显式地调用 来停止接收通知,但是当 发送到已释放观察者的事件也会停止。

属性返回的 返回列表中所有部分和 NSManagedObject 项的全拷贝 struct。这对于管理状态是非常理想的,因为它们是线程安全的,并且不会受到结果集进一步更改的影响。 会自动更新其 snapshot 值至获取的最新状态。

(有关此方法的响应式编程变体,请参阅关于 Combine publishers 的章节)

不同(请参阅下方的 示例), 不跟踪详细插入、删除和移动。作为交换, 更加轻量级,旨在与 DiffableDataSource.TableViewAdapterDiffableDataSource.CollectionViewAdapter 一起良好工作。

self.dataSource = DiffableDataSource.CollectionViewAdapter<Person>(
    collectionView: self.collectionView,
    dataStack: CoreStoreDefaults.dataStack,
    cellProvider: { (collectionView, indexPath, person) in
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PersonCell") as! PersonCell
        cell.setPerson(person)
        return cell
    }
)

// ...

listPublisher.addObserver(self) { [weak self] (listPublisher) in
   self?.dataSource?.apply(
       listPublisher.snapshot, animatingDifferences: true
   )
}

观察列表的详细更改

如果您需要跟踪每个对象的插入、删除、移动和更新,请实现一个 协议并指定 EntityType

class MyViewController: UIViewController, ListObserver {
    func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
        // ...
    }
    
    func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
        // ...
    }
}

除了 ,您还可以根据处理更改通知的详细程度实现三种观察者协议

  • 允许您处理以下回调方法
    func listMonitorWillChange(_ monitor: ListMonitor<MyPersonEntity>)
    func listMonitorDidChange(_ monitor: ListMonitor<MyPersonEntity>)
    func listMonitorWillRefetch(_ monitor: ListMonitor<MyPersonEntity>)
    func listMonitorDidRefetch(_ monitor: ListMonitor<MyPersonEntity>)

listMonitorDidChange(_:)listMonitorDidRefetch(_:) 的实现都是必需的。listMonitorDidChange(_:) 的计数、顺序或过滤对象发生变化时被调用。listMonitorDidRefetch(_:) 在执行 或内部持久存储变更时被调用。

  • 除了 方法外,还允许您处理对象的插入、更新和删除
    func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didInsertObject object: MyPersonEntity, toIndexPath indexPath: IndexPath)
    func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didDeleteObject object: MyPersonEntity, fromIndexPath indexPath: IndexPath)
    func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didUpdateObject object: MyPersonEntity, atIndexPath indexPath: IndexPath)
    func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didMoveObject object: MyPersonEntity, fromIndexPath: IndexPath, toIndexPath: IndexPath)
  • 除了 方法外,还允许您处理部分的插入和删除
    func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int)
    func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int)

接下来,我们需要创建一个 实例,并将我们的 注册为观察者

self.monitor = dataStack.monitorList(
    From<MyPersonEntity>()
        .where(\.age > 30)
        .orderBy(.ascending(\.name))
        .tweak { $0.fetchBatchSize = 20 }
)
self.monitor.addObserver(self)

类似于 ,一个 也可以注册多个 到单个

如果您已经注意到了,monitorList(...)方法接受类似于获取的WhereOrderByTweak子句。由于ListMonitor维护的列表需要具有确定性排序,因此至少需要FromOrderBy子句。

monitorList(...)创建的ListMonitor将维护一个单分区列表。因此,您可以仅使用索引访问其内容

let firstPerson = self.monitor[0]

如果需要将列表分组到分区中,请使用monitorSectionedList(...)方法和SectionBy子句创建ListMonitor实例

self.monitor = dataStack.monitorSectionedList(
    From<MyPersonEntity>()
        .sectionBy(\.age)
        .where(\.gender == "M")
        .orderBy(.ascending(\.age), .ascending(\.name))
        .tweak { $0.fetchBatchSize = 20 }
)

以这种方式创建的列表控制器将根据SectionBy子句指示的属性键分组对象。还有一个需要记住的是,OrderBy子句应该以这种方式对列表进行排序,即SectionBy属性将一起排序(这是NSFetchedResultsController所共享的要求。)

SectionBy子句还可以传递闭包来将分区名称转换为可显示的字符串

self.monitor = dataStack.monitorSectionedList(
    From<MyPersonEntity>()
        .sectionBy(\.age) { (sectionName) -> String? in
            "\(sectionName) years old"
        }
        .orderBy(.ascending(\.age), .ascending(\.name))
)

这在实现UITableViewDelegate的分区标题时非常有用

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    let sectionInfo = self.monitor.sectionInfoAtIndex(section)
    return sectionInfo.name
}

要访问分组列表的对象,请使用IndexPath或元组

let indexPath = IndexPath(row: 2, section: 1)
let person1 = self.monitor[indexPath]
let person2 = self.monitor[1, 2]
// person1 and person2 are the same object

类型安全的CoreStoreObject

从CoreStore 4.0开始,我们现在可以创建持久化对象而无需依赖于.xcdatamodeld Core Data文件。新的CoreStoreObject子类取代了NSManagedObject,在这些类上声明的特别类型化属性将合成Core Data属性。

class Animal: CoreStoreObject {
    @Field.Stored("species")
    var species: String = ""
}

class Dog: Animal {
    @Field.Stored("nickname")
    var nickname: String?
    
    @Field.Relationship("master")
    var master: Person?
}

class Person: CoreStoreObject {
    @Field.Stored("name")
    var name: String = ""
    
    @Field.Relationship("pets", inverse: \Dog.$master)
    var pets: Set<Dog>
}

要保存到Core Data的属性名称指定为keyPath参数。这使得我们可以重构Swift代码,而不影响底层数据库。例如

class Person: CoreStoreObject {
    @Field.Stored("name")
    private var internalName: String = ""
    // note property name is independent of the storage key name
}

在这里,我们使用了属性名internalName并将其设置为private,但底层键路径"name"没有改变,因此我们的模型不会触发数据迁移。

要告知DataStack这些类型,请将所有CoreStoreObject的实体添加到CoreStoreSchema

CoreStoreDefaults.dataStack = DataStack(
    CoreStoreSchema(
        modelVersion: "V1",
        entities: [
            Entity<Animal>("Animal", isAbstract: true),
            Entity<Dog>("Dog"),
            Entity<Person>("Person")
        ]
    )
)
CoreStoreDefaults.dataStack.addStorage(/* ... */)

这就是CoreStore构建模型所需的所有内容;我们不再需要.xcdatamodeld文件。

此外,还可以使用@Field属性创建类型安全的键路径字符串

let keyPath = String(keyPath: \Dog.$nickname)

以及WhereOrderBy子句

let puppies = try dataStack.fetchAll(
    From<Dog>()
        .where(\.$age < 5)
        .orderBy(.ascending(\.$age))
)

适用于NSManagedObject的所有CoreStore API也适用于CoreStoreObject。这包括ListMonitorImportableObject、获取等。

新的 @Field 属性包装语法

⚠️重要提示: @Field 属性仅支持 CoreStoreObject 子类。如果你正在使用 NSManagedObject,则需要继续使用 @NSManaged 为你的属性。

从 CoreStore 7.1.0 版本开始,CoreStoreObject 属性可以被转换为 @Field 属性包装。

‼️在转换之前,请注意以下警告,否则模型哈希可能发生变化。

如果转换风险过高,当前的 Value.RequiredValue.OptionalTransformable.RequiredTransformable.OptionalRelationship.ToOneRelationship.ToManyOrderedRelationship.ToManyUnordered 将继续支持,你可以选择现在继续使用它们。

‼️这一点非常重要,请在转换前务必设置方案中的 VersionLock

@Field.Stored

@Field.Stored 属性包装用于持久化值类型。这是 "非暂时性" Value.RequiredValue.Optional 属性的替代。

之前
@Field.Stored
class Person: CoreStoreObject {
    
    let title = Value.Required<String>("title", initial: "Mr.")
    let nickname = Value.Optional<String>("nickname")
}
class Person: CoreStoreObject {
    
    @Field.Stored("title")
    var title: String = "Mr."
    
    @Field.Stored("nickname")
    var nickname: String?
}

⚠️只有非暂时性的 Value.RequiredValue.Optional 才能转换为 Field.Stored。对于暂时性/计算属性,请参阅下一节的 @Field.Virtual 属性。⚠️转换时,请确保所有参数,包括默认值,都是完全相同的,否则模型哈希可能发生变化。

@Field.Virtual

@Field.Virtual 属性包装用于未保存的计算值类型。这是 "暂时性" Value.RequiredValue.Optional 属性的替代。

之前
@Field.Virtual
class Animal: CoreStoreObject {
    
    let speciesPlural = Value.Required<String>(
        "speciesPlural",
        transient: true,
        customGetter: Animal.getSpeciesPlural(_:)
    )
    
    let species = Value.Required<String>("species", initial: "")
    
    static func getSpeciesPlural(_ partialObject: PartialObject<Animal>) -> String? {
        let species = partialObject.value(for: { $0.species })
        return species + "s"
    }
}
class Animal: CoreStoreObject {
    
    @Field.Virtual(
        "speciesPlural",
        customGetter: { (object, field) in
            return object.$species.value + "s"
        }
    )
    var speciesPlural: String
    
    @Field.Stored("species")
    var species: String = ""
}

⚠️只有是瞬时的Value.RequiredValue.Optional,才能转换成Field.Virtual。对于非瞬时的属性,请参考上一节中的@Field.Stored属性。⚠️转换时,请确保所有参数,包括默认值,都是完全相同的,否则模型哈希可能发生变化。

@Field.Coded

用于二进制可编码值的@Field.Coded属性包装器。这是Transformable.RequiredTransformable.Optional属性的新对等物,而非替代物。@Field.Coded还支持其他编码,如JSON和自定义二进制转换器。

‼️当前的Transformable.RequiredTransformable.Optional机制无法安全地一对一转换为@Field.Coded。请仅使用@Field.Coded来对新添加的属性。

之前
@Field.Coded
class Vehicle: CoreStoreObject {
    
    let color = Transformable.Optional<UIColor>("color", initial: .white)
}
class Vehicle: CoreStoreObject {
    
    @Field.Coded("color", coder: FieldCoders.NSCoding.self)
    var color: UIColor? = .white
}

提供了内置编码器,如FieldCoders.NSCodingFieldCoders.JsonFieldCoders.Plist,并支持自定义编码/解码。

class Person: CoreStoreObject {
    
    struct CustomInfo: Codable {
        // ...
    }
    
    @Field.Coded("otherInfo", coder: FieldCoders.Json.self)
    var otherInfo: CustomInfo?
    
    @Field.Coded(
        "photo",
        coder: {
            encode: { $0.toData() },
            decode: { Photo(fromData: $0) }
        }
    )
    var photo: Photo?
}

‼️重要:编码器/解码器的任何更改都不会反映在VersionLock中,因此请确保编码器和解码器逻辑与持久存储的所有版本兼容。

@Field.Relationship

用于与其他CoreStoreObject的链接关系的@Field.Relationship属性包装器。这是Relationship.ToOneRelationship.ToManyOrderedRelationship.ToManyUnordered属性的一个替代品。

关系的类型由@Field.Relationship泛型类型决定

  • Optional<T>:一对一关系
  • Array<T>:多对有序关系
  • Set<T>:多对无序关系
之前
@Field.Stored
class Pet: CoreStoreObject {
    
    let master = Relationship.ToOne<Person>("master")
}
class Person: CoreStoreObject {
    
    let pets: Relationship.ToManyUnordered<Pet>("pets", inverse: \.$master)
}
class Pet: CoreStoreObject {
    
    @Field.Relationship("master")
    var master: Person?
}
class Person: CoreStoreObject {
    
    @Field.Relationship("pets", inverse: \.$master)
    var pets: Set<Pet>
}

⚠️转换时,请确保所有参数,包括默认值,都是完全相同的,否则模型哈希可能发生变化。

同时请注意,如何通过inverse:参数静态地链接Relationship。**所有关系都必须有一个"inverse"关系**。遗憾的是,由于Swift编译器限制,我们只可以声明一对关系中的一个inverse:

《@Field》使用说明

访问器语法

在使用key-path工具时,使用《@Field》属性包装器的属性必须使用《$》语法

  • 之前:From<Person>.where(\.title == "Mr.")
  • 之后:From<Person>.where(\.$title == "Mr.")

这适用于使用《ObjectPublisher》和《ObjectSnapshot》进行属性访问。

  • 之前:let name = personSnapshot.name
  • 之后:let name = personSnapshot.$name

默认值与初始值

为《CoreStoreObject》属性分配默认值时,一个常见的错误是在创建对象时为其分配一个值并期望其被评估

//
class Person: CoreStoreObject {

    @Field.Stored("identifier")
    var identifier: UUID = UUID() // Wrong!
    
    @Field.Stored("createdDate")
    var createdDate: Date = Date() // Wrong!
}

这个默认值只会在与《DataStack》设置模式时被评估,所有实例最终将具有相同的值。这种“默认值”的语法通常仅用于实际合理的常数值,或像《""》或《0》这样的监视值。

对于实际的“初始值”,《@Field.Stored》和《@Field.Coded》现在支持通过《dynamicInitialValue:参数在对象创建期间进行动态评估

//
class Person: CoreStoreObject {

    @Field.Stored("identifier", dynamicInitialValue: { UUID() })
    var identifier: UUID
    
    @Field.Stored("createdDate", dynamicInitialValue: { Date() })
    var createdDate: Date
}

使用此功能时,不应分配“默认值”(即没有《=`表达式)。

《VersionLock》

虽然能够在代码中声明实体非常方便,但担心我们可能会意外更改《CoreStoreObject》的属性并破坏用户的模型版本历史。为此,《CoreStoreSchema》允许我们将属性“锁定”到特定配置。对那个《VersionLock》的任何更改将在《CoreStoreSchema》初始化期间引发断言失败,这样你就可以查找更改《VersionLock》哈希的提交。

要使用《VersionLock》,创建《CoreStoreSchema》,启动应用,并在控制台查找类似的自动打印日志消息

VersionLock

复制此字典值并将其用作《CoreStoreSchema》初始化器的《versionLock:参数

CoreStoreSchema(
    modelVersion: "V1",
    entities: [
        Entity<Animal>("Animal", isAbstract: true),
        Entity<Dog>("Dog"),
        Entity<Person>("Person"),
    ],
    versionLock: [
        "Animal": [0x1b59d511019695cf, 0xdeb97e86c5eff179, 0x1cfd80745646cb3, 0x4ff99416175b5b9a],
        "Dog": [0xe3f0afeb109b283a, 0x29998d292938eb61, 0x6aab788333cfc2a3, 0x492ff1d295910ea7],
        "Person": [0x66d8bbfd8b21561f, 0xcecec69ecae3570f, 0xc4b73d71256214ef, 0x89b99bfe3e013e8b]
    ]
)

你还可以在《DataStack》完全设置后通过打印到控制台得到此哈希

print(CoreStoreDefaults.dataStack.modelSchema.printCoreStoreSchema())

一旦设置版本锁定,对属性或模型所做的任何更改都将触发类似于下列断言失败的行为

VersionLock failure

RxCoreStore外部模块获取。

设置部分。

dataStack.reactive
    .addStorage(
        SQLiteStore(fileName: "core_data.sqlite")
    )
    .sink(
        receiveCompletion: { result in
            // ...
        },
        receiveValue: { (progress) in
            print("\(round(progress.fractionCompleted * 100)) %") // 0.0 ~ 1.0
            switch progress {
            case .migrating(let storage, let nsProgress):
                // ...
            case .finished(let storage, let migrationRequired):
                // ...
            }
        }
    )
    .store(in: &cancellables)

事务也可通过DataStack.reactive.perform(_:)作为发布者获得,它返回一个发出闭包参数返回的任何类型的Combine Future

dataStack.reactive
    .perform(
        asynchronous: { (transaction) -> (inserted: Set<NSManagedObject>, deleted: Set<NSManagedObject>) in

            // ...
            return (
                transaction.insertedObjects(),
                transaction.deletedObjects()
            )
        }
    )
    .sink(
        receiveCompletion: { result in
            // ...
        },
        receiveValue: { value in
            let inserted = dataStack.fetchExisting(value0.inserted)
            let deleted = dataStack.fetchExisting(value0.deleted)
            // ...
        }
    )
    .store(in: &cancellables)

为了方便导入,ImportableObjectImportableUniqueObjects可以直接通过DataStack.reactive.import[Unique]Object(_:source:)DataStack.reactive.import[Unique]Objects(_:sourceArray:)导入,无需创建事务块。在这种情况下,发布者发出可以直接从主队列使用的对象。

dataStack.reactive
    .importUniqueObjects(
        Into<Person>(),
        sourceArray: [
            ["name": "John"],
            ["name": "Bob"],
            ["name": "Joe"]
        ]
    )
    .sink(
        receiveCompletion: { result in
            // ...
        },
        receiveValue: { (people) in
            XCTAssertEqual(people?.count, 3)
            // ...
        }
    )
    .store(in: &cancellables)

ListPublisher.reactive

ListPublisher 可以通过使用 ListPublisher.reactive.snapshot(emitInitialValue:) 在 Combine 中发送 ListSnapshot。快照值在主队列中发送。

listPublisher.reactive
    .snapshot(emitInitialValue: true)
    .sink(
        receiveCompletion: { result in
            // ...
        },
        receiveValue: { (listSnapshot) in
            dataSource.apply(
                listSnapshot,
                animatingDifferences: true
            )
        }
    )
    .store(in: &cancellables)

ObjectPublisher.reactive

ObjectPublisher 可以通过使用 ObjectPublisher.reactive.snapshot(emitInitialValue:) 在 Combine 中发送 ObjectSnapshot。快照值在主队列中发送。

objectPublisher.reactive
    .snapshot(emitInitialValue: true)
    .sink(
        receiveCompletion: { result in
            // ...
        },
        receiveValue: { (objectSnapshot) in
            tableViewCell.setObject(objectSnapshot)
        }
    )
    .store(in: &tableViewCell.cancellables)

SwiftUI 工具

在 SwiftUI 中观察列表和对象更改可通过几种方法完成。一种是通过创建自动更新其内容的视图,或通过声明触发视图更新的属性包装器。这两种方法在内部实现几乎相同,但这使得您可以根据自定义 View 的结构灵活选择。

SwiftUI 视图

CoreStore 提供了在数据更改时自动更新其内容的 View 容器。

列表阅读器

列表阅读器观察对列表发布者的变化并动态创建其内容视图。构建闭包接受一个可以用于创建内容的列表快照值。

let people: ListPublisher<Person>

var body: some View {
   List {
       ListReader(self.people) { listSnapshot in
           ForEach(objectIn: listSnapshot) { person in
               // ...
           }
       }
   }
   .animation(.default)
}

如上所示,典型用例是将其与CoreStore的ForEach扩展一起使用。

也可以可选地提供KeyPath来提取列表快照的特定属性。

let people: ListPublisher<Person>

var body: some View {
    ListReader(self.people, keyPath: \.count) { count in
        Text("Number of members: \(count)")
    }
}

对象读取器

对象读取器观察对对象发布者的变化并动态创建其内容视图。构建闭包接受一个可以用于创建内容的对象快照值。

let person: ObjectPublisher<Person>

var body: some View {
   ObjectReader(self.person) { objectSnapshot in
       // ...
   }
   .animation(.default)
}

可以可选地提供KeyPath来提取对象快照的特定属性。

let person: ObjectPublisher<Person>

var body: some View {
    ObjectReader(self.person, keyPath: \.fullName) { fullName in
        Text("Name: \(fullName)")
    }
}

默认情况下,当观察的对象从存储中删除时,对象读取器不会创建其视图。在这种情况下,可以通过提供自定义的视图来使用placeholder:参数显示当对象被删除时的视图。

let person: ObjectPublisher<Person>

var body: some View {
   ObjectReader(
       self.person,
       content: { objectSnapshot in
           // ...
       },
       placeholder: { Text("Record not found") }
   )
}

SwiftUI 属性包装器

作为列表阅读器对象读取器的替代方案,CoreStore 还提供了属性包装器,当数据变化时触发视图更新。

列表状态

@列表状态属性暴露出一个会自动更新到最新更改的列表快照值。

@ListState
var people: ListSnapshot<Person>

init(listPublisher: ListPublisher<Person>) {
   self._people = .init(listPublisher)
}

var body: some View {
   List {
       ForEach(objectIn: self.people) { objectSnapshot in
           // ...
       }
   }
   .animation(.default)
}

如上所示,典型用例是将其与CoreStore的ForEach扩展一起使用。

如果尚未提供ListPublisher实例,可以通过提供获取条款和DataStack实例来在线执行获取。这样做可以在没有初始值的情况下声明属性。

@ListState(
    From<Person>()
        .sectionBy(\.age)
        .where(\.isMember == true)
        .orderBy(.ascending(\.lastName))
)
var people: ListSnapshot<Person>

var body: some View {
    List {
        ForEach(sectionIn: self.people) { section in
            Section(header: Text(section.sectionID)) {
                ForEach(objectIn: section) { person in
                    // ...
                }
            }
        }
    }
    .animation(.default)
}

有关其他初始化变体,请参阅ListState.swift源文档。

ObjectState

一个@ObjectState属性公开了一个可选的ObjectSnapshot值,它会自动更新到最新的更改。

@ObjectState
var person: ObjectSnapshot<Person>?

init(objectPublisher: ObjectPublisher<Person>) {
   self._person = .init(objectPublisher)
}

var body: some View {
   HStack {
       if let person = self.person {
           AsyncImage(person.$avatarURL)
           Text(person.$fullName)
       }
       else {
           Text("Record removed")
       }
   }
}

如上所示,如果对象已被删除,属性的值将是nil,因此如果需要,可以使用它来显示占位符。

SwiftUI 扩展

为了方便起见,CoreStore 为标准 SwiftUI 类型提供了扩展。

ForEach

有几个ForEach初始化方法重载可用。根据您的输入数据和预期闭包数据选择。请参考下表(注意参数标签,因为它们很重要)。

数据示例
签名
ForEach(_: [ObjectSnapshot<O>])
闭包
ObjectSnapshot<O>
let array: [ObjectSnapshot<Person>]

var body: some View {
    
    List {
        
        ForEach(self.array) { objectSnapshot in
            
            // ...
        }
    }
}
签名
ForEach(objectIn: ListSnapshot<O>)
闭包
ObjectPublisher<O>
let listSnapshot: ListSnapshot<Person>

var body: some View {
    
    List {
        
        ForEach(objectIn: self.listSnapshot) { objectPublisher in
            
            // ...
        }
    }
}
签名
ForEach(objectIn: [ObjectSnapshot<O>])
闭包
ObjectPublisher<O>
let array: [ObjectSnapshot<Person>]

var body: some View {
    
    List {
        
        ForEach(objectIn: self.array) { objectPublisher in
            
            // ...
        }
    }
}
签名
ForEach(sectionIn: ListSnapshot<O>)
闭包
[ListSnapshot<O>.SectionInfo]
let listSnapshot: ListSnapshot<Person>

var body: some View {
    
    List {
        
        ForEach(sectionIn: self.listSnapshot) { sectionInfo in
            
            // ...
        }
    }
}
签名
ForEach(objectIn: ListSnapshot<O>.SectionInfo)
闭包
ObjectPublisher<O>
let listSnapshot: ListSnapshot<Person>

var body: some View {
    
    List {
        
        ForEach(sectionIn: self.listSnapshot) { sectionInfo in
            
            ForEach(objectIn: sectionInfo) { objectPublisher in
               
                // ...
            }
        }
    }
}

路线图

原型设计阶段

  • 小部件/扩展存储共享支持
  • 支持CloudKit

待考虑

  • 派生属性
  • 跨存储关系(通过抓取属性)

安装

  • 需求
  • 依赖项
  • 其他备注
    • 请关闭应用中的调试参数com.apple.CoreData.ConcurrencyDebug。CoreStore通过将主上下文设置为只读,并按顺序执行事务,已经为您保证了安全性。

使用CocoaPods安装

在您的Podfile中,添加

pod 'CoreStore', '~> 9.1'

并运行

pod update

这将作为框架安装CoreStore。在您的swift文件中声明import CoreStore以使用库。

使用 Carthage 安装

在您的 Cartfile 中添加

github "JohnEstropia/CoreStore" >= 9.1.0

并运行

carthage update

这将作为框架安装CoreStore。在您的swift文件中声明import CoreStore以使用库。

使用 Swift Package Manager 安装

dependencies: [
    .package(url: "https://github.com/JohnEstropia/CoreStore.git", from: "9.1.0"))
]

在您的 swift 文件中声明 import CoreStore 以使用该库。

作为 Git Submodule 安装

git submodule add https://github.com/JohnEstropia/CoreStore.git <destination directory>

CoreStore.xcodeproj 拖放到您的项目中。

通过 Xcode 的 Swift Package Manager 安装

文件 - Swift 包 - 添加包依赖… 菜单中,搜索

CoreStore

其中 JohnEstropia所有者 (可能也会出现分支)。然后将其添加到您的项目中

更改集

欲查看完整更新日志,请参阅发布页面。

联系

您可以在Twitter上联系我 @JohnEstropia

或者加入我们的Slack团队 swift-corestore.slack.com

同时提供日语支持,欢迎咨询!

谁使用CoreStore?

我很乐意了解正在使用CoreStore的应用程序。请给我发送消息,我会欢迎任何反馈!

许可

CoreStore遵循MIT许可证发布。有关更多信息,请参阅LICENSE文件。