利用 Swift 的优雅和安全性释放 Core Data 的真正力量
依赖管理器
联系方式
从之前的 CoreStore 版本升级?查看 🆕 功能,并确保阅读变更日志。
CoreStore 是 Swift 源代码兼容性项目的一部分。
纯 Swift 模型
class Person: CoreStoreObject {
@Field.Stored("name")
var name: String = ""
@Field.Relationship("pets", inverse: \Dog.$master)
var pets: Set<Dog>
}
(也支持经典的 NSManagedObject
)
设置支持渐进式迁移
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_是有原因的。阅读详细信息!
查看 Demo 应用程序项目以获取代码示例!
CoreStore 受到(并且正在受到)开发数据相关应用程序的实际需求的极大影响。它强制执行安全和方便的 Core Data 使用,同时让您利用行业鼓励的最佳实践。
ListPublisher
和 ObjectPublisher
现在具有它们的 @ListState
和 @ObjectState
SwiftUI 属性包装器。Combine Publisher
也可以通过 ListPublisher.reactive
、ObjectPublisher.reactive
和 DataStack.reactive
命名空间使用。UITableViews
和 UICollectionViews
现在有了一个新的盟友:ListPublisher
提供可区分的快照,使重新加载动画变得非常简单和非常安全。告别 UITableViews
和 UICollectionViews
重新加载错误!NSManagedObjectContext
是严格只读的,而所有更新都是通过串行 *事务* 完成的。(参见保存和处理事务)min
、max
等)和原始属性值现在同样方便。(参见获取和查询)NSFetchedResultsController
和 KVO 的负担。作为额外的奖励,列表和对象可观察类型都支持多个观察者。这意味着您可以让多个视图控制器有效地共享单个资源!(参见观察更改和通知)NSManagedObject
,但它提供了 CoreStoreObject
,其子类可以在 Swift 代码中声明类型安全的属性,而无需维护单独的资源文件来存储模型。作为奖励,这些特殊属性支持自定义类型,并且可以用于创建类型安全的键路径和查询。(参见类型安全的 CoreStoreObject
)DataStack
版本字符串(MigrationChain
)的序列,CoreStore 将在需要时自动使用渐进式迁移。(参见迁移)CoreStoreLogger
协议实现。(参见日志记录和错误报告)DataStack
中管理单独的存储,就像 _.xcdatamodeld_ 配置的设计方式一样。 CoreStore 也会默认管理一个栈,但您可以根据需要创建和管理任意数量的栈。(参见设置)NSManagedObject
子类名称相同。 CoreStore 从托管对象模型文件加载实体到类的映射,因此您可以为实体及其类名分配独立的名称。有可以使其他 Core Data 用户受益的想法吗? 欢迎提出 功能请求!
为了获得最大的安全性和性能,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
兼具两者的优点,将主要的 NSManagedObjectContext
视为只读上下文(或“viewContext”),并且仅允许在子上下文上的 _事务_ 中进行更改
这允许一个非常流畅的主线程,同时仍然可以利用安全的嵌套上下文。
初始化 CoreStore 的最简单方法是将默认存储添加到默认栈
try CoreStoreDefaults.dataStack.addStorageAndWait()
这个单行代码执行以下操作
DataStack
触发 CoreStoreDefaults.dataStack
的延迟初始化NSPersistentStoreCoordinator
、根保存 NSManagedObjectContext
和只读的主 NSManagedObjectContext
SQLiteStore
,文件名为 "[应用程序包名称].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
Combine 发布者 的部分中进行了详细说明)
您可能最常用的 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
Combine 发布者 中解释)
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 目前支持以下 schema 类
XcodeDataModelSchema
:一个模型版本,其中实体从 *.xcdatamodeld
* 文件加载。CoreStoreSchema
:使用 CoreStoreObject
实体创建的模型版本。(请参阅 类型安全的 CoreStoreObject
s)UnsafeDataModelSchema
:使用现有的 NSManagedObjectModel
实例创建的模型版本。所有模型版本的 DynamicSchema
然后被收集到单个 SchemaHistory
实例中,然后传递给 DataStack
。以下是一些常见的用例
分组在 *.xcdatamodeld
* 文件中的多个模型版本(Core Data 标准方法)
CoreStoreDefaults.dataStack = DataStack(
xcodeModelName: "MyModel",
bundle: Bundle.main,
migrationChain: ["MyAppModel", "MyAppModelV2", "MyAppModelV3", "MyAppModelV4"]
)
基于 CoreStoreSchema
的模型版本(无需 *.xcdatamodeld
* 文件)(有关更多详细信息,另请参见 类型安全的 CoreStoreObject
s)
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")
]
)
)
过去应用程序版本中 *.xcdatamodeld
* 文件中的模型,但已迁移到新的 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
,指示成功或失败。
(此方法的反应式编程变体将在下一节关于 DataStack
Combine 发布者 中进一步解释)
请注意,此方法还会返回一个可选的 Progress
。 如果 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 使用 Core Data 的默认自动迁移机制。 换句话说,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"
])
这允许根据起始版本使用不同的迁移路径。 上面的示例解析为以下路径
使用空值(nil
、[]
或 [:]
)初始化指示 DataStack
禁用渐进式迁移并恢复为默认迁移行为(即,使用 *.xcdatamodeld
* 的当前版本作为最终版本)
let dataStack = DataStack(migrationChain: nil)
MigrationChain
在传递给 DataStack
时会经过验证,除非它是空的,否则如果满足以下任何条件,将引发断言
⚠️ 重要提示:如果指定了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
:默认映射提供程序,它尝试通过搜索来自 Bundle.allBundles
的所有 *.xcmappingmodel
* 文件,或尽可能地依赖轻量级迁移来推断两个 DynamicSchema
版本之间的模型迁移。这些映射提供程序符合 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
// ...
}
)
事务闭包在闭包完成后自动保存更改。 要取消并回滚事务,请通过调用 try transaction.cancel()
从闭包内部抛出 CoreStoreError.userCancelled
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:...)
,但实际上您可以使用 3 种类型的事务:异步、同步和不安全。
从 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(...)
返回NSManagedObjectContext
总是会失败。 CoreStore 在有意义的时候(而不是在保存期间)在创建时为您检查这一点。如果该实体存在于多个配置中,则需要为目标持久化存储提供配置名称
let person = transaction.create(Into<MyPersonEntity>("Config1"))
或者如果持久化存储是自动生成的“Default”配置,则指定 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,该唯一 ID 将用于区分在调用 importUniqueObject(...)
时应创建新对象还是应更新现有对象。两种协议都要求实现者指定一个 ImportSource
,它可以设置为对象可以从中提取数据的任何类型
typealias ImportSource = NSDictionary
typealias ImportSource = [String: Any]
typealias ImportSource = NSData
您甚至可以使用来自流行的第三方 JSON 库的外部类型,或者只是简单的元组或原始类型。
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]
类型作为 source
的参数来调用 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:)
中 throw
。
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
协议可让您指定一个“唯一 ID”,事务可以使用该 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:)
方法中 throw
错误来取消所有对象。
在我们深入了解之前,请注意 CoreStore 区分获取和查询
commit()
之前。)以下情况使用获取NSManagedObject
或 CoreStoreObject
实例NSString
、NSNumber
、Int
、NSDate
、键值 NSDictionary
或任何符合 QueryableAttributeType
的类型。(有关内置类型的列表,请参见 *QueryableAttributeType.swift*)获取和查询的搜索条件使用子句指定。所有获取和查询都需要一个 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
或者如果持久化存储是自动生成的“Default”配置,则指定 nil
let person = try dataStack.fetchAll(From<MyPersonEntity>(nil))
现在我们知道如何使用 From
子句了,让我们继续获取和查询。
目前有 5 种获取方法可以从 CoreStore
、DataStack
实例或 BaseDataTransaction
实例调用。以下所有方法都接受相同的参数:必需的 From
子句和可选的一系列 Where
、OrderBy
和/或 Tweak
子句。
fetchAll(...)
- 返回与条件匹配的所有对象的数组。fetchOne(...)
- 返回与条件匹配的第一个对象。fetchCount(...)
- 返回与条件匹配的对象的数量。fetchObjectIDs(...)
- 返回与条件匹配的所有对象的 NSManagedObjectID
的数组。fetchObjectID(...)
- 返回与条件匹配的第一个对象的 NSManagedObjectID
。每个方法的用途都很简单,但是我们需要了解如何设置获取的子句。
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 builders。我们还可以使用 Swift 的 Smart 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
子句是 CoreStore 的 NSSortDescriptor
包装器。使用它来指定属性键,以对获取(或查询)结果进行排序。
var mostValuablePeople = try dataStack.fetchAll(
From<MyPersonEntity>(),
OrderBy<MyPersonEntity>(.descending("rating"), .ascending("surname"))
)
如上所示,OrderBy
接受一个 SortKey
枚举值列表,该列表可以是 .ascending
或 .descending
。与 Where
子句一样,OrderBy
子句也是泛型类型。为了避免冗长的泛型对象类型重复,fetch 方法支持Fetch Chain builders。我们还可以使用 Swift 的 Smart 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
)
Tweak
子句允许您,呃,调整 fetch(或查询)。Tweak
在闭包中公开 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
}
)
Tweak
也支持 Fetch Chain builders。
var people = try dataStack.fetchAll(
From<MyPersonEntity>(),
.where(\.age > 30)
.orderBy(.ascending(\.surname))
.tweak {
$0.includesPendingChanges = false
$0.returnsObjectsAsFaults = false
$0.includesSubentities = false
}
)
子句的评估顺序与它们在 fetch/query 中出现的顺序一致,因此您通常需要将 Tweak
设置为最后一个子句。 Tweak
的闭包仅在 fetch 发生之前执行,因此请确保闭包捕获的任何值都不容易出现竞争条件。
虽然 Tweak
允许您微配置 NSFetchRequest
,但请注意,CoreStore 已经将该 NSFetchRequest
预配置为合适的默认值。只有在您知道自己在做什么时才使用 Tweak
!
原始属性获取是其他 Core Data 包装器库忽略的功能之一。如果您熟悉 NSDictionaryResultType
和 -[NSFetchedRequest propertiesToFetch]
,您可能知道设置原始值和聚合值的查询是多么痛苦。 CoreStore 通过公开以下两种方法使其变得容易。
queryValue(...)
- 返回属性或聚合值的单个原始值。如果存在多个结果,queryValue(...)
仅返回第一个项目。queryAttributes(...)
- 返回包含属性键及其对应值的字典数组。上述两种方法都接受相同的参数:必需的 From
子句、必需的 Select<T>
子句,以及可选的一系列 Where
、OrderBy
、GroupBy
和/或 Tweak
子句。
设置 From
、Where
、OrderBy
和 Tweak
子句与 fetch 时的设置方式类似。 对于查询,您还需要知道如何使用 Select<T>
和 GroupBy
子句。
Select<T>
子句指定目标属性/聚合键,以及预期的返回类型。
let johnsAge = try dataStack.queryValue(
From<MyPersonEntity>(),
Select<Int>("age"),
Where<MyPersonEntity>("name == %@", "John Smith")
)
上面的例子查询符合 Where
条件的第一个对象的“age”属性。 johnsAge
将绑定到 Int?
类型,如 Select<Int>
泛型类型所示。 对于 queryValue(...)
,符合 QueryableAttributeType
的类型允许作为返回类型(因此也作为 Select<T>
的泛型类型)。
对于 queryAttributes(...)
,只有 NSDictionary
对 Select
有效,因此您可以省略泛型类型。
let allAges = try dataStack.queryAttributes(
From<MyPersonEntity>(),
Select("age")
)
query 方法也支持 Query Chain builders。我们还可以使用 Swift 的 Smart KeyPaths 在表达式中使用。
let johnsAge = try dataStack.queryValue(
From<MyPersonEntity>()
.select(\.age) // binds the result to Int
.where(\.name == "John Smith")
)
如果您只需要特定属性的值,则可以只指定键名(就像我们对 Select<Int>("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
]
]
"count(friends)"
键名由 CoreStore 自动使用,但如果需要,您可以指定自己的键别名。
let personJSON = try dataStack.queryAttributes(
From<MyPersonEntity>(),
Select("name", .count("friends", as: "friendsCount"))
)
现在返回
[
[
"name": "John Smith",
"friendsCount": 42
],
[
"name": "Jane Doe",
"friendsCount": 231
]
]
GroupBy
子句允许您按指定的属性/聚合对结果进行分组。 这仅对 queryAttributes(...)
有用,因为 queryValue(...)
仅返回第一个值。
let personJSON = try dataStack.queryAttributes(
From<MyPersonEntity>(),
Select("age", .count("age", as: "count")),
GroupBy("age")
)
GroupBy
子句也是泛型类型,并支持 Query Chain builders。我们还可以使用 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()
这样做会将所有日志记录调用路由到您的记录器。
请注意,为了保持调用堆栈信息的完整性,对这些方法的所有调用都 NOT 是线程管理的。因此,您必须确保您的记录器是线程安全的,否则您可能必须将您的日志记录实现分派到串行队列。
在实现 CoreStoreLogger
的 assert(...)
和 abort(...)
函数时,请特别小心。
assert(...)
:DEBUG
和发布版本之间,或 -O
和 -Onone
之间的行为,都留给实现者负责。 CoreStore 仅对无效但通常可恢复的错误调用 CoreStoreLogger.assert(...)
(例如,可能导致在其他地方引发和处理错误的早期验证失败)。abort(...)
:此方法是您的应用程序同步记录 CoreStore 中致命错误的最后机会。 在调用此函数后(CoreStore 内部调用 fatalError()
)应用程序将立即终止。所有 CoreStore 类型都具有非常有用的(且格式精美!)print(...)
输出。 几个例子,ListMonitor
CoreStoreError.mappingModelNotFoundError
:
这些都是使用 CustomDebugStringConvertible.debugDescription
实现的,因此它们也适用于 lldb 的 po
命令。
CoreStore 为观察托管对象提供类型安全的包装器。
🆕ObjectPublisher | ObjectMonitor | 🆕ListPublisher | ListMonitor | |
---|---|---|---|---|
对象数量 | 1 | 1 | N | N |
允许多个观察者 | ✅ | ✅ | ✅ | ✅ |
发出细粒度的更改 | ❌ | ✅ | ❌ | ✅ |
发出 DiffableDataSource 快照 | ✅ | ❌ | ✅ | ❌ |
委托方法 | ❌ | ✅ | ❌ | ✅ |
闭包回调 | ✅ | ❌ | ✅ | ❌ |
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
Combine 发布者 部分中详细说明)
如果您需要专门跟踪对象中更改的属性,请实现 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
实例共享到不同的屏幕。
您可以通过 ObjectMonitor
的 object
属性获取 ObjectMonitor
的对象。 如果对象被删除,object
属性将变为 nil
以防止进一步访问。
虽然 ObjectMonitor
也公开了 removeObserver(...)
,但它仅存储观察者的 weak
引用,并且会安全地取消注册已释放的观察者。
每当其 fetch 的结果集发生更改时,ListPublisher
的观察者都可以收到通知。 您可以通过从 DataStack
fetch 来创建 ListPublisher
。
let listPublisher = dataStack.listPublisher(
From<Person>()
.sectionBy(\.age") { "Age \($0)" } // sections are optional
.where(\.title == "Engineer")
.orderBy(.ascending(\.lastName))
)
要接收通知,请调用 ListPublisher
的 addObserve(...)
方法,并传递回调闭包的所有者。
listPublisher.addObserver(self) { [weak self] (listPublisher) in
let snapshot: ListSnapshot<Person> = listPublisher.snapshot
// handle changes
}
请注意,所有者实例不会被保留。 您可以显式调用 ListPublisher.removeObserver(...)
来停止接收通知,但 ListPublisher
也会停止向已释放的观察者发送事件。
从 ListPublisher.snapshot
属性返回的 ListSnapshot
返回列表中所有 section 和 NSManagedObject
项目的完整副本 struct
。 这非常适合管理状态,因为它们是线程安全的,并且不受对结果集的进一步更改的影响。 ListPublisher
会自动将其 snapshot
值更新为 fetch 的最新状态。
(有关此方法的反应式编程变体将在 ListPublisher
Combine 发布者 部分中详细说明)
与 ListMonitor
s 不同(请参阅下面的 ListMonitor
示例),ListPublisher
不跟踪详细的插入、删除和移动。 作为回报,ListPublisher
更轻量级,并且旨在与 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
)
}
如果您需要跟踪每个对象的插入、删除、移动和更新,请实现其中一个 ListObserver
协议并指定 EntityType
。
class MyViewController: UIViewController, ListObserver {
func listMonitorDidChange(monitor: ListMonitor<MyPersonEntity>) {
// ...
}
func listMonitorDidRefetch(monitor: ListMonitor<MyPersonEntity>) {
// ...
}
}
包括 ListObserver
在内,您可以根据需要处理更改通知的详细程度来实现 3 个观察者协议。
ListObserver
:让您处理这些回调方法 func listMonitorWillChange(_ monitor: ListMonitor<MyPersonEntity>)
func listMonitorDidChange(_ monitor: ListMonitor<MyPersonEntity>)
func listMonitorWillRefetch(_ monitor: ListMonitor<MyPersonEntity>)
func listMonitorDidRefetch(_ monitor: ListMonitor<MyPersonEntity>)
listMonitorDidChange(_:)
和 listMonitorDidRefetch(_:)
方法都需要实现。当 ListMonitor
的数量、顺序或过滤后的对象发生变化时,会调用 listMonitorDidChange(_:)
。当 ListMonitor.refetch()
被执行或内部持久化存储发生变化时,会调用 listMonitorDidRefetch(_:)
。
ListObjectObserver
:除了 ListObserver
的方法之外,还允许你处理对象的插入、更新和删除。 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)
ListSectionObserver
:除了 ListObjectObserver
的方法之外,还允许你处理 section 的插入和删除。 func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didInsertSection sectionInfo: NSFetchedResultsSectionInfo, toSectionIndex sectionIndex: Int)
func listMonitor(_ monitor: ListMonitor<MyPersonEntity>, didDeleteSection sectionInfo: NSFetchedResultsSectionInfo, fromSectionIndex sectionIndex: Int)
然后我们需要创建一个 ListMonitor
实例,并将我们的 ListObserver
注册为观察者。
self.monitor = dataStack.monitorList(
From<MyPersonEntity>()
.where(\.age > 30)
.orderBy(.ascending(\.name))
.tweak { $0.fetchBatchSize = 20 }
)
self.monitor.addObserver(self)
类似于 ObjectMonitor
,一个 ListMonitor
也可以注册多个 ListObserver
。
如果你注意到了,monitorList(...)
方法接受 Where
、OrderBy
和 Tweak
子句,就像一个 fetch 操作一样。由于 ListMonitor
维护的列表需要具有确定性的顺序,因此至少需要 From
和 OrderBy
子句。
从 monitorList(...)
创建的 ListMonitor
将维护一个单 section 的列表。因此,你可以仅使用索引来访问其内容。
let firstPerson = self.monitor[0]
如果列表需要分组为多个 section,请使用 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
子句也可以传递一个闭包,将 section 名称转换为可显示的字符串。
self.monitor = dataStack.monitorSectionedList(
From<MyPersonEntity>()
.sectionBy(\.age) { (sectionName) -> String? in
"\(sectionName) years old"
}
.orderBy(.ascending(\.age), .ascending(\.name))
)
这在实现 UITableViewDelegate
的 section header 时非常有用。
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let sectionInfo = self.monitor.sectionInfoAtIndex(section)
return sectionInfo.name
}
要访问 section 列表的对象,请使用 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
从 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
、fetching 等。
⚠️ 重要提示:@Field
属性仅支持CoreStoreObject
子类。 如果您正在使用NSManagedObject
,则需要继续使用@NSManaged
作为您的属性。
从 CoreStore 7.1.0 开始,CoreStoreObject
属性可以转换为 @Field
Property Wrappers。
‼️ 在转换之前,请注意以下警告,否则模型的哈希值可能会发生变化。
如果转换风险太大,当前的 Value.Required
、Value.Optional
、Transformable.Required
、Transformable.Optional
、Relationship.ToOne
、Relationship.ToManyOrdered
和 Relationship.ToManyUnordered
将暂时全部支持,因此您可以选择暂时按原样使用它们。
‼️ 怎么强调都不为过,但在转换之前,请务必设置 schema 的VersionLock
!
@Field.Stored
property wrapper 用于持久化的值类型。 这是 “非瞬态” 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?
}
|
⚠️ 只有 NOT transient 值的Value.Required
和Value.Optional
可以转换为Field.Stored
。 对于瞬态/计算属性,请参阅下一节中的@Field.Virtual
属性。⚠️ 转换时,请确保所有参数(包括默认值)完全相同,否则模型的哈希值可能会发生变化。
@Field.Virtual
property wrapper 用于未保存的计算值类型。 这是 “瞬态” 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 = ""
}
|
⚠️ 只有 ARE transient 值的Value.Required
和Value.Optional
可以转换为Field.Virtual
。 对于非瞬态属性,请参阅上一节中的@Field.Stored
属性。⚠️ 转换时,请确保所有参数(包括默认值)完全相同,否则模型的哈希值可能会发生变化。
@Field.Coded
property wrapper 用于二进制可编码值。 这是 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
property wrapper 用于与其他 CoreStoreObject
的链接关系。 这是 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>
}
|
⚠️ 转换时,请确保所有参数(包括默认值)完全相同,否则模型的哈希值可能会发生变化。
另请注意 Relationship
如何使用 inverse:
参数静态链接。 **所有关系都需要具有“逆”关系**。 遗憾的是,由于 Swift 编译器限制,我们只能在其中一个关系对上声明 inverse:
。
访问器语法
使用键路径实用程序时,使用 @Field
property wrappers 的属性需要使用 $
语法。
From<Person>.where(\.title == "Mr.")
From<Person>.where(\.$title == "Mr.")
这适用于使用 ObjectPublisher
和 ObjectSnapshot
进行属性访问。
let name = personSnapshot.name
let name = personSnapshot.$name
默认值 vs. 初始值
将默认值分配给 CoreStoreObject
属性时,一个常见的错误是分配一个值并期望在每次创建对象时对其进行评估。
// ❌
class Person: CoreStoreObject {
@Field.Stored("identifier")
var identifier: UUID = UUID() // Wrong!
@Field.Stored("createdDate")
var createdDate: Date = Date() // Wrong!
}
仅当 DataStack
设置 schema 时才会评估此默认值,并且所有实例最终都将具有相同的值。 这种 “默认值” 的语法通常仅用于实际合理的常量值,或 sentinel 值,例如 ""
或 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
}
使用此功能时,不应分配 “默认值”(即,没有 =
表达式)。
虽然能够仅在代码中声明实体很方便,但令人担忧的是,我们可能会意外更改 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 publishers 可从 DataStack
、ListPublisher
和 ObjectPublisher
的 .reactive
命名空间属性获得。
通过 DataStack.reactive.addStorage(_:)
添加存储会返回一个 publisher,该 publisher 报告一个 MigrationProgress
enum
值。 仅当存储进行迁移时才会发出 .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)
Transactions 也可通过 DataStack.reactive.perform(_:)
作为 publishers 提供,该 publisher 返回一个 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)
为了方便导入,可以直接通过 DataStack.reactive.import[Unique]Object(_:source:)
和 DataStack.reactive.import[Unique]Objects(_:sourceArray:)
导入 ImportableObject
和 ImportableUniqueObjects
,而无需创建 transaction 块。 在这种情况下,publisher 会发出可以直接从主队列使用的对象。
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
通过 Combine 使用 ListPublisher.reactive.snapshot(emitInitialValue:)
来发出 ListSnapshot
。 快照值在主队列中发出。
listPublisher.reactive
.snapshot(emitInitialValue: true)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (listSnapshot) in
dataSource.apply(
listSnapshot,
animatingDifferences: true
)
}
)
.store(in: &cancellables)
可以使用 ObjectPublisher
通过 Combine 使用 ObjectPublisher.reactive.snapshot(emitInitialValue:)
来发出 ObjectSnapshot
。 快照值在主队列中发出。
objectPublisher.reactive
.snapshot(emitInitialValue: true)
.sink(
receiveCompletion: { result in
// ...
},
receiveValue: { (objectSnapshot) in
tableViewCell.setObject(objectSnapshot)
}
)
.store(in: &tableViewCell.cancellables)
可以通过几种方法在 SwiftUI 中观察列表和对象的变化。 一种方法是创建自动更新其内容的视图,或者通过声明触发视图更新的 property wrappers。 这两种方法在内部几乎以相同的方式实现,但这使您可以根据自定义 View
的结构灵活地进行操作。
CoreStore 提供了 View
容器,可以在数据更改时自动更新其内容。
ListReader
观察 ListPublisher
的变化,并动态创建其内容视图。 构建器闭包接收一个 ListSnapshot
值,可用于创建内容。
let people: ListPublisher<Person>
var body: some View {
List {
ListReader(self.people) { listSnapshot in
ForEach(objectIn: listSnapshot) { person in
// ...
}
}
}
.animation(.default)
}
如上所示,一个典型的用例是将其与 CoreStore 的 ForEach
扩展一起使用。
还可以选择提供 KeyPath
来提取 ListSnapshot
的特定属性
let people: ListPublisher<Person>
var body: some View {
ListReader(self.people, keyPath: \.count) { count in
Text("Number of members: \(count)")
}
}
ObjectReader
观察 ObjectPublisher
的变化并动态创建其内容视图。构建器闭包接收一个 ObjectSnapshot
值,可用于创建内容
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(self.person) { objectSnapshot in
// ...
}
.animation(.default)
}
还可以选择提供 KeyPath
来提取 ObjectSnapshot
的特定属性
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(self.person, keyPath: \.fullName) { fullName in
Text("Name: \(fullName)")
}
}
默认情况下,当观察的对象从存储中删除时,ObjectReader
不会创建其视图。 在这些情况下,可以使用 placeholder:
参数来提供在对象被删除时显示的自定义 View
let person: ObjectPublisher<Person>
var body: some View {
ObjectReader(
self.person,
content: { objectSnapshot in
// ...
},
placeholder: { Text("Record not found") }
)
}
作为 ListReader
和 ObjectReader
的替代方案,CoreStore 还提供属性包装器,当数据更改时触发视图更新。
@ListState
属性公开一个 ListSnapshot
值,该值会自动更新到最新的更改。
@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
实例尚不可用,可以通过提供 fetch 子句和 DataStack
实例来内联完成 fetch。 这样做可以声明属性而无需初始值
@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
属性公开一个可选的 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
,因此可以用于在需要时显示占位符。
为方便起见,CoreStore 提供了对标准 SwiftUI 类型的扩展。
提供了多个 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
// ...
}
}
}
}
|
com.apple.CoreData.ConcurrencyDebug
调试参数。 CoreStore 已经通过使主上下文只读以及仅串行执行事务来保证您的安全。在你的 Podfile
中,添加
pod 'CoreStore', '~> 9.1'
并运行
pod update
这会将 CoreStore 安装为一个框架。 在你的 swift 文件中声明 import CoreStore
以使用该库。
在你的 Cartfile
中,添加
github "JohnEstropia/CoreStore" >= 9.3.0
并运行
carthage update
这会将 CoreStore 安装为一个框架。 在你的 swift 文件中声明 import CoreStore
以使用该库。
dependencies: [
.package(url: "https://github.com/JohnEstropia/CoreStore.git", from: "9.3.0"))
]
在你的 swift 文件中声明 import CoreStore
以使用该库。
git submodule add https://github.com/JohnEstropia/CoreStore.git <destination directory>
将 CoreStore.xcodeproj 拖放到你的项目。
从 File - Swift Packages - Add Package Dependency… 菜单中,搜索
CoreStore
其中 JohnEstropia
是 *Owner* (fork 也可能会出现)。 然后添加到你的项目
有关完整的 Changelog,请参阅 Releases 页面。
您可以在 Twitter 上联系我 @JohnEstropia
或者加入我们的 Slack 团队 swift-corestore.slack.com
日语也支持,欢迎提问!
我很乐意听到有关使用 CoreStore 的应用程序的信息。 请给我留言,我会欢迎任何反馈!
CoreStore 在 MIT 许可下发布。 有关更多信息,请参阅 LICENSE 文件