利用 Swift 的优雅性和安全性释放 Core Data 的真实力量
依赖管理器
联系
- Swift 5.7: iOS 13+ / macOS 10.15+ / watchOS 7.4+ / tvOS 13.0+
- 先前支持的 Swift 版本:Swift 5.5,Swift 5.4,Swift 5.3
从先前的 CoreStore 版本升级?查看
CoreStore 是 Swift 源兼容性项目 的一部分。
内容
- TL;DR (即示例代码)
- 为什么使用 CoreStore?
- 架构
- CoreStore 教程(所有这些都在 示例 应用程序项目中提供演示!)
- 路线图
- 安装
- 变更集
- 联系
- 谁在使用 CoreStore?
- 许可证
TL;DR (简称示例代码)
纯 Swift 模型
class Person: CoreStoreObject {
@Field.Stored("name")
var name: String = ""
@Field.Relationship("pets", inverse: \Dog.$master)
var pets: Set<Dog>
}
(也支持经典 NSManagedObject
s)
使用支持渐进式迁移来设置
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 的同时,让您能够利用行业推崇的最佳实践。
特性
🆕 SwiftUI 和 Combine API 工具。强项ListPublisher
和ObjectPublisher
现在拥有它们的@ListState
和@ObjectState
SwiftUI 属性包装器。结合Publisher
也通过ListPublisher.reactive
、ObjectPublisher.reactive
和DataStack.reactive
命名空间提供。- 向后兼容的 DiffableDataSources 实现!
UITableViews
和UICollectionViews
现在有了新盟友:ListPublisher
提供可比较的快照,使得重新加载动画变得非常容易和安全。告别UITableViews
和UICollectionViews
重新加载错误吧! 💎 围绕 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”的
从子上下文保存到父上下文可以保证上下文间数据的最大完整性,而不会阻塞主队列。但是 实际上,合并上下文通常比保存上下文要快得多。CoreStore 的“DataStack”结合了两者之长,将主上下文作为只读上下文(或“视图上下文”)处理,并且只允许在子上下文上的 事务 中进行更改
这样可以使主线程更加流畅,同时仍可以利用安全的嵌套上下文。
设置
初始化 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
在我们上面的示例代码中,请注意,你不需要做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
对象,但你可以使用CoreStoreObject
和CoreStoreSchema
完全从源代码创建模型。要使用此功能,请参阅类型安全的CoreStoreObject
。
请注意,在我们之前的示例中,addStorageAndWait(_:)
和addStorage(_:completion:)
都接受InMemoryStore
或SQLiteStore
。这两个实现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
组合发布者部分。)
本地存储
你可能会使用最普遍的StorageInterface
是SQLiteStore
,它将数据保存在本地的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
}
如果你有自定义的NSIncrementalStore
或NSAtomicStore
子类,你可以实现此协议并像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版本名称或CoreStoreSchema
的modelName
)。
对于更复杂的非线性迁移路径,您还可以传入映射键值到源-目标版本的版本树。
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
// ...
}
)
对于 DataStack
的 MigrationChain
中存在但未由 SQLiteStore
的 migrationMappingProviders
数组中的任何提供程序处理的版本迁移,CoreStore 会自动尝试使用 InferredSchemaMappingProvider
作为后备方案。最后,如果 InferredSchemaMappingProvider
无法解决任何映射,迁移将失败,并且 DataStack.addStorage(...)
方法将报告失败。
对于 CustomSchemaMappingProvider
,通过动态对象 UnsafeSourceObject
和 UnsafeDestinationObject
支持更细粒度的更新。以下示例允许迁移有条件地忽略一些对象。
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)
}
)
⚠️ 在事务闭包中返回NSManagedObject
或CoreStoreObject
时请小心。这些实例仅用于事务本身。请参阅安全传递对象。
从 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 }
)
CoreStore
、DataStack
和BaseDataTransaction
具有非常灵活的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(...)
还适用于多个NSManagedObject
、CoreStoreObject
或NSManagedObjectID
。
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 }
)
为了支持实体的数据导入,请在NSManagedObject
或CoreStoreObject
子类上实现ImportableObject
或ImportableUniqueObject
。
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 }
)
这样做会使事务通过迭代导入源数组并调用 ImportableObject
的 shouldInsert(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()
之前。)。当需要以下情况时使用获取:- 结果需要是
NSManagedObject
或CoreStoreObject
实例 - 在搜索中需要包含未保存的对象(尽管可以配置获取以排除它们)
- 结果需要是
- 查询直接从持久存储中提取数据。这意味着当计算汇总(例如,计数、最小值、最大值等)时搜索更快。当需要以下情况时使用查询:
- 需要计算汇总函数(以下将列出支持的函数列表)
- 结果可以是原始值,如
NSString
、NSNumber
、Int
、NSDate
,NSDictionary
的键值对或任何符合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
子句,接下来让我们看看如何获取和查询。
获取
当前从 CoreStore
、DataStack
实例或 BaseDataTransaction
实例中,您可以调用 5 种获取方法。以下所有方法都接受相同的参数:一个必需的 From
子句,以及一系列可选的 Where
、OrderBy
和/或 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
子句还实现了 &&
、||
和 !
逻辑运算符,因此您可以在不写出太多的 AND
、OR
和 NOT
字符串的情况下提供逻辑条件。
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>
子句,以及可选的一系列Where
、OrderBy
、GroupBy
和/或Tweak
子句。
设置From
、Where
、OrderBy
和Tweak
子句的方式与获取类似。对于查询,您还需要了解如何使用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
做的那样),但也可以使用几个聚合函数作为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()
这样做将所有日志调用通道到您的日志记录器。
请注意,为了保留调用堆栈信息,对上述方法的调用**不**受线程管理。因此,您必须确保您的日志记录器是线程安全的,或者您可能必须将日志实现调度到一个串行队列中。
在实现 CoreStoreLogger
的 assert(...)
和 abort(...)
函数时,请特别小心
assert(...)
:在DEBUG
和发布版本、或-O
和-Onone
之间的行为,均留给实现者自行负责。CoreStore 只调用CoreStoreLogger.assert(...)
处理无效但通常可恢复的错误(例如,较早的验证失败可能导致在别处抛出和处理的错误)abort(...)
:此方法是应用在 CoreStore 中同步记录一个致命错误的最后一个机会。在该函数被调用后,应用将被终止(CoreStore 内部调用fatalError()
)
所有 CoreStore 类型都有非常实用的(格式也很美观的!)print(...)
输出。以下是一些示例,如 ListMonitor
:CoreStoreError.mappingModelNotFoundError
这些都是通过 CustomDebugStringConvertible.debugDescription
实现的,因此它们也可以和 lldb 的 po
命令一起使用。
监视更改和通知
CoreStore 为监视托管对象提供了类型安全的包装器
ObjectMonitor | 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)
或从listPublisher
的ListSnapshot
索引
let listPublisher: ListPublisher<Person> = // ...
// ...
let objectPublisher = listPublisher.snapshot[indexPath]
(请参阅下文ListPublisher
示例)
要接收通知,请调用ObjectPublisher
的addObserve(...)
方法并传递回调闭包的所有者
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
值至获取的最新状态。
(有关此方法的响应式编程变体,请参阅关于
与 DiffableDataSource.TableViewAdapter
和 DiffableDataSource.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(...)
方法接受类似于获取的Where
、OrderBy
和Tweak
子句。由于ListMonitor
维护的列表需要具有确定性排序,因此至少需要From
和OrderBy
子句。
从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)
以及Where
和OrderBy
子句
let puppies = try dataStack.fetchAll(
From<Dog>()
.where(\.$age < 5)
.orderBy(.ascending(\.$age))
)
适用于NSManagedObject
的所有CoreStore API也适用于CoreStoreObject
。这包括ListMonitor
、ImportableObject
、获取等。
@Field
属性包装语法
新的
⚠️ 重要提示:@Field
属性仅支持CoreStoreObject
子类。如果你正在使用NSManagedObject
,则需要继续使用@NSManaged
为你的属性。
从 CoreStore 7.1.0 版本开始,CoreStoreObject
属性可以被转换为 @Field
属性包装。
‼️ 在转换之前,请注意以下警告,否则模型哈希可能发生变化。
如果转换风险过高,当前的 Value.Required
、Value.Optional
、Transformable.Required
、Transformable.Optional
、Relationship.ToOne
、Relationship.ToManyOrdered
和 Relationship.ToManyUnordered
将继续支持,你可以选择现在继续使用它们。
‼️ 这一点非常重要,请在转换前务必设置方案中的VersionLock
!
@Field.Stored
@Field.Stored
属性包装用于持久化值类型。这是 "非暂时性" Value.Required
和 Value.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.Required
和Value.Optional
才能转换为Field.Stored
。对于暂时性/计算属性,请参阅下一节的@Field.Virtual
属性。⚠️ 转换时,请确保所有参数,包括默认值,都是完全相同的,否则模型哈希可能发生变化。
@Field.Virtual
@Field.Virtual
属性包装用于未保存的计算值类型。这是 "暂时性" Value.Required
和 Value.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.Required
和Value.Optional
,才能转换成Field.Virtual
。对于非瞬时的属性,请参考上一节中的@Field.Stored
属性。⚠️ 转换时,请确保所有参数,包括默认值,都是完全相同的,否则模型哈希可能发生变化。
@Field.Coded
用于二进制可编码值的@Field.Coded
属性包装器。这是Transformable.Required
和Transformable.Optional
属性的新对等物,而非替代物。@Field.Coded
还支持其他编码,如JSON和自定义二进制转换器。
‼️ 当前的Transformable.Required
和Transformable.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.NSCoding
、FieldCoders.Json
和FieldCoders.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.ToOne
、Relationship.ToManyOrdered
和Relationship.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》,启动应用,并在控制台查找类似的自动打印日志消息
复制此字典值并将其用作《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())
一旦设置版本锁定,对属性或模型所做的任何更改都将触发类似于下列断言失败的行为
RxSwift实用程序可通过RxCoreStore外部模块获取。
Combine发布者可通过(DataStack, ListPublisher, 和 ObjectPublisher)'s .reactive 命名空间属性获取。
通过DataStack.reactive.addStorage(_:)
添加存储返回一个报告MigrationProgress
枚举值的发布者。只有当存储通过迁移时,才发出.migrating
值。有关存储设置过程的详细信息,请参阅设置部分。
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)
为了方便导入,ImportableObject
和ImportableUniqueObjects
可以直接通过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文件。