CoreStore

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

Build Status Last Commit Platform License

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

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

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

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

目录

TL;DR(又名代码示例)

纯 Swift 模型

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

(也支持经典的 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?

CoreStore 受到(并且正在受到)开发数据相关应用程序的实际需求的极大影响。它强制执行安全和方便的 Core Data 使用,同时让您利用行业鼓励的最佳实践。

特性

有可以使其他 Core Data 用户受益的想法吗? 欢迎提出 功能请求

架构

为了获得最大的安全性和性能,CoreStore 将强制执行其设计的编码模式和实践。(别担心,它并不像听起来那么可怕。)但是在您在应用程序中使用它之前,最好了解 CoreStore 的“魔力”。

如果您已经熟悉 CoreData 的内部工作原理,这是 CoreStore 抽象的映射

Core Data CoreStore
NSPersistentContainer
(.xcdatamodeld 文件)
DataStack
NSPersistentStoreDescription
(.xcdatamodeld 文件中的“配置”)
StorageInterface 实现
InMemoryStoreSQLiteStore
NSManagedObjectContext BaseDataTransaction 子类
SynchronousDataTransactionAsynchronousDataTransactionUnsafeDataTransaction

许多 Core Data 包装器库以这种方式设置其 NSManagedObjectContext

nested contexts

从子上下文到根上下文的嵌套保存确保了上下文之间的最大数据完整性,而不会阻塞主队列。但是实际上,合并上下文仍然比保存上下文快得多。 CoreStore 的 DataStack 兼具两者的优点,将主要的 NSManagedObjectContext 视为只读上下文(或“viewContext”),并且仅允许在子上下文上的 _事务_ 中进行更改

nested contexts and merge hybrid

这允许一个非常流畅的主线程,同时仍然可以利用安全的嵌套上下文。

设置

初始化 CoreStore 的最简单方法是将默认存储添加到默认栈

try CoreStoreDefaults.dataStack.addStorageAndWait()

这个单行代码执行以下操作

在大多数情况下,此配置就足够了。但是对于更硬核的设置,请参考这个广泛的示例

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

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

💡如果您从未听说过“配置”,您会在您的 _.xcdatamodeld_ 文件中找到它们 xcode configurations screenshot

在上面的示例代码中,请注意您不需要执行 CoreStoreDefaults.dataStack = dataStack 行。 您也可以像下面这样保留对 DataStack 的引用,并直接调用它的所有实例方法

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

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

请注意,在前面的示例中,addStorageAndWait(_:)addStorage(_:completion:) 都接受 InMemoryStoreSQLiteStore。 这些实现了 StorageInterface 协议。

内存存储

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

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

异步变体

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

(此方法的响应式编程变体在有关 DataStack Combine 发布者 的部分中进行了详细说明)

本地存储

您可能最常用的 StorageInterfaceSQLiteStore,它将数据保存在本地 SQLite 文件中。

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

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

CoreStore 可以决定这些属性的默认值,因此 SQLiteStore 可以无需任何参数即可初始化

try dataStack.addStorageAndWait(SQLiteStore())

(此方法的异步变体将在下一节关于 迁移 中进一步解释,反应式编程变体将在 DataStack 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
}

如果您有自定义的 NSIncrementalStoreNSAtomicStore 子类,您可以实现此协议,并以类似于 SQLiteStore 的方式使用它。

迁移

声明模型版本

模型版本现在表示为一流协议 DynamicSchema。 CoreStore 目前支持以下 schema 类

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

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

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

基于 CoreStoreSchema 的模型版本(无需 *.xcdatamodeld* 文件)(有关更多详细信息,另请参见 类型安全的 CoreStoreObjects

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 将尝试迁移现有的持久性存储,直到它与 SchemaHistorycurrentModelVersion 匹配。 如果未找到从存储的版本到数据模型的版本的映射模型路径,CoreStore 将放弃并报告错误。

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

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

最常见的用法是以递增的顺序传入模型版本(NSManagedObject 的 *.xcdatamodeld* 版本名称,或 CoreStoreSchemamodelName),如上所示。

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

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

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

使用空值(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 提供了几种声明迁移映射的方法

这些映射提供程序符合 SchemaMappingProvider,并且可以传递给 SQLiteStore 的初始化程序

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

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

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

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

UnsafeSourceObject 是源模型版本中存在的对象的只读代理。 UnsafeDestinationObject 是一个读写对象,它(可选)插入到目标模型版本中。 这两个类的属性通过键值编码访问。

保存和处理事务

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

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

事务闭包在闭包完成后自动保存更改。 要取消并回滚事务,请通过调用 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)
    }
)

⚠️从事务闭包返回 NSManagedObjectCoreStoreObject 时要小心。 这些实例仅供事务使用。 请参阅安全传递对象

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

同步事务

perform(synchronous:...) 创建。 虽然语法与其异步对应项相似,但 perform(synchronous:...) 会等待其事务块完成,然后才返回

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

上面的 transaction 是一个 SynchronousDataTransaction 实例。

由于 perform(synchronous:...) 在技术上会阻塞两个队列(调用者的队列和事务的后台队列),因此认为它不太安全,因为它更容易发生死锁。 特别注意闭包不要阻塞任何其他外部队列。

默认情况下,perform(synchronous:...) 将等待诸如 ListMonitor 之类的观察者在方法返回之前收到通知。 这可能会导致死锁,特别是如果您从主线程调用它。 为了降低这种风险,您可以尝试将 waitForAllObservers: 参数设置为 false。 这样做会告诉 SynchronousDataTransaction 仅阻塞到它完成保存为止。 它不会等待其他上下文接收这些更改。 这降低了死锁风险,但可能会产生令人惊讶的副作用

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

由于同步事务的复杂性,如果您的应用具有非常高的事务吞吐量,强烈建议您使用异步事务

不安全事务

的特殊之处在于它们不将更新封闭在闭包中

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

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

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

这允许非连续的更新。请注意,这种灵活性是有代价的:您现在负责管理事务的并发性。正如本叔叔所说,“能力越大,竞争条件就越多。”

如上面的例子所示,使用不安全事务时,可以多次调用 commit()

您已经了解了如何创建事务,但我们还没有看到如何进行创建更新删除。上面的 3 种事务都是 BaseDataTransaction 的子类,它实现了如下所示的方法。

创建对象

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

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

虽然语法很简单,但 CoreStore 不仅仅是简单地插入一个新对象。这一行代码执行以下操作

如果该实体存在于多个配置中,则需要为目标持久化存储提供配置名称

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 }
)

但是 CoreStoreDataStackBaseDataTransaction 都有非常灵活的 fetchExisting(...) 方法,您可以使用它来回传递实例

let jane: MyPersonEntity = // ...

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

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

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

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

导入数据

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

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

如果您有很多属性,您不想每次想要导入数据时都重复这种映射。 CoreStore 允许您只编写一次数据映射代码,而您所要做的就是通过 BaseDataTransaction 子类调用 importObject(...)importUniqueObject(...)

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

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

两种协议都要求实现者指定一个 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] 类型作为 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

通常,我们不会每次导入数据时都不断创建对象。通常,我们还需要更新已经存在的对象。实现 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 区分获取查询

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

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

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

现在我们知道如何使用 From 子句了,让我们继续获取和查询。

获取

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

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

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 builders。我们还可以使用 Swift 的 Smart KeyPaths 作为 Where 子句表达式

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

Where 子句还实现了 &&||! 逻辑运算符,因此您可以提供逻辑条件,而无需编写太多的 ANDORNOT 字符串

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

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

OrderBy 子句

OrderBy 子句是 CoreStore 的 NSSortDescriptor 包装器。使用它来指定属性键,以对获取(或查询)结果进行排序。

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

如上所示,OrderBy 接受一个 SortKey 枚举值列表,该列表可以是 .ascending.descending。与 Where 子句一样,OrderBy 子句也是泛型类型。为了避免冗长的泛型对象类型重复,fetch 方法支持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 子句

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 通过公开以下两种方法使其变得容易。

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

设置 FromWhereOrderByTweak 子句与 fetch 时的设置方式类似。 对于查询,您还需要知道如何使用 Select<T>GroupBy 子句。

Select<T> 子句

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(...),只有 NSDictionarySelect 有效,因此您可以省略泛型类型。

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 的参数。

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 子句

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 是线程管理的。因此,您必须确保您的记录器是线程安全的,否则您可能必须将您的日志记录实现分派到串行队列。

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

所有 CoreStore 类型都具有非常有用的(且格式精美!)print(...) 输出。 几个例子,ListMonitor

screen shot 2016-07-10 at 22 56 44

CoreStoreError.mappingModelNotFoundError:

MappingModelNotFoundError

这些都是使用 CustomDebugStringConvertible.debugDescription 实现的,因此它们也适用于 lldb 的 po 命令。

观察更改和通知

CoreStore 为观察托管对象提供类型安全的包装器。

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

观察单个属性

要获取对象中单个属性更改的通知,有两种方法,具体取决于对象的基类。

let observer = person.observe(\.age, options: [.new]) { (person, change)
    print("Happy \(change.newValue)th birthday!")
}
let observer = person.age.observe(options: [.new]) { (person, change)
    print("Happy \(change.newValue)th birthday!")
}

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

观察单个对象的更新

如果对象的任何属性发生更改,ObjectPublisher 的观察者可以收到通知。 您可以直接从对象创建一个 ObjectPublisher

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

或者通过索引 ListPublisherListSnapshot

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

(请参阅下面的 ListPublisher 示例

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

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

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

ObjectPublisher.snapshot 属性返回的 ObjectSnapshot 返回对象所有属性的完整副本 struct。 这非常适合管理状态,因为它们是线程安全的,并且不受对实际对象的进一步更改的影响。 ObjectPublisher 会自动将其 snapshot 值更新为对象的最新状态。

(有关此方法的反应式编程变体将在 ObjectPublisher 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 实例共享到不同的屏幕。

您可以通过 ObjectMonitorobject 属性获取 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))
)

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

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 发布者 部分中详细说明)

ListMonitors 不同(请参阅下面的 ListMonitor 示例),ListPublisher 不跟踪详细的插入、删除和移动。 作为回报,ListPublisher 更轻量级,并且旨在与 DiffableDataSource.TableViewAdapterDiffableDataSource.CollectionViewAdapter 一起使用。

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

// ...

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

观察详细的列表更改

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

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

包括 ListObserver 在内,您可以根据需要处理更改通知的详细程度来实现 3 个观察者协议。

    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(_:)

    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)

然后我们需要创建一个 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(...) 方法接受 WhereOrderByTweak 子句,就像一个 fetch 操作一样。由于 ListMonitor 维护的列表需要具有确定性的顺序,因此至少需要 FromOrderBy 子句。

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

类型安全的 CoreStoreObject

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

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

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

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

要保存到 Core Data 的属性名称被指定为 keyPath 参数。 这使我们能够在不影响底层数据库的情况下重构我们的 Swift 代码。 例如:

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

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

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

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

这就是 CoreStore 构建模型所需的一切; **我们不再需要 .xcdatamodeld 文件了。**

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

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

以及 WhereOrderBy 子句。

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

所有可与 NSManagedObject 一起使用的 CoreStore API 也可用于 CoreStoreObject。 这些包括 ListMonitorImportableObject、fetching 等。

新的 @Field 属性包装器语法

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

从 CoreStore 7.1.0 开始,CoreStoreObject 属性可以转换为 @Field Property Wrappers。

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

如果转换风险太大,当前的 Value.RequiredValue.OptionalTransformable.RequiredTransformable.OptionalRelationship.ToOneRelationship.ToManyOrderedRelationship.ToManyUnordered 将暂时全部支持,因此您可以选择暂时按原样使用它们。

‼️怎么强调都不为过,但在转换之前,请务必设置 schema 的 VersionLock

@Field.Stored

@Field.Stored property wrapper 用于持久化的值类型。 这是 “非瞬态” Value.RequiredValue.Optional 属性的替代品。

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

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

@Field.Virtual

@Field.Virtual property wrapper 用于未保存的计算值类型。 这是 “瞬态” Value.RequiredValue.Optional 属性的替代品。

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

⚠️只有 ARE transient 值的 Value.RequiredValue.Optional 可以转换为 Field.Virtual。 对于非瞬态属性,请参阅上一节中的 @Field.Stored 属性。⚠️转换时,请确保所有参数(包括默认值)完全相同,否则模型的哈希值可能会发生变化。

@Field.Coded

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

‼️当前的 Transformable.RequiredTransformable.Optional 机制没有到 @Field.Coded 的安全的一对一转换。 请仅对新添加的属性使用 @Field.Coded

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

内置编码器(例如 FieldCoders.NSCodingFieldCoders.JsonFieldCoders.Plist)可用,并且还支持自定义编码/解码。

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

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

@Field.Relationship

@Field.Relationship property wrapper 用于与其他 CoreStoreObject 的链接关系。 这是 Relationship.ToOneRelationship.ToManyOrderedRelationship.ToManyUnordered 属性的替代品。

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

之前:
@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 使用说明

访问器语法

使用键路径实用程序时,使用 @Field property wrappers 的属性需要使用 $ 语法。

这适用于使用 ObjectPublisherObjectSnapshot 进行属性访问。

默认值 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
}

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

VersionLock

虽然能够仅在代码中声明实体很方便,但令人担忧的是,我们可能会意外更改 CoreStoreObject 的属性并破坏用户的模型版本历史记录。 为此,CoreStoreSchema 允许我们将属性 “锁定” 到特定配置。 对该 VersionLock 的任何更改都将在 CoreStoreSchema 初始化期间引发断言失败,因此您可以查找更改 VersionLock 哈希的提交。

要使用 VersionLock,请创建 CoreStoreSchema,运行该应用,并查找自动打印到控制台的类似日志消息:

VersionLock

复制此字典值并将其用作 CoreStoreSchema 初始化程序的 versionLock: 参数。

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

您还可以在 DataStack 完全设置完毕后通过打印到控制台来获取此哈希。

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

设置版本锁后,对属性或模型的任何更改都将触发类似于此的断言失败:

VersionLock failure

响应式编程

RxSwift

RxSwift 实用程序可通过 RxCoreStore 外部模块获得。

Combine

Combine publishers 可从 DataStackListPublisherObjectPublisher.reactive 命名空间属性获得。

DataStack.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:) 导入 ImportableObjectImportableUniqueObjects,而无需创建 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.reactive

可以使用 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.reactive

可以使用 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 实用程序

可以通过几种方法在 SwiftUI 中观察列表和对象的变化。 一种方法是创建自动更新其内容的视图,或者通过声明触发视图更新的 property wrappers。 这两种方法在内部几乎以相同的方式实现,但这使您可以根据自定义 View 的结构灵活地进行操作。

SwiftUI 视图

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

ListReader

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

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") }
   )
}

SwiftUI 属性包装器

作为 ListReaderObjectReader 的替代方案,CoreStore 还提供属性包装器,当数据更改时触发视图更新。

ListState

@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

@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
               
                // ...
            }
        }
    }
}

路线图

原型阶段

正在考虑中

安装

使用 CocoaPods 安装

在你的 Podfile 中,添加

pod 'CoreStore', '~> 9.1'

并运行

pod update

这会将 CoreStore 安装为一个框架。 在你的 swift 文件中声明 import CoreStore 以使用该库。

使用 Carthage 安装

在你的 Cartfile 中,添加

github "JohnEstropia/CoreStore" >= 9.3.0

并运行

carthage update

这会将 CoreStore 安装为一个框架。 在你的 swift 文件中声明 import CoreStore 以使用该库。

使用 Swift Package Manager 安装

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

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

作为 Git Submodule 安装

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

CoreStore.xcodeproj 拖放到你的项目。

通过 Xcode 的 Swift Package Manager 安装

File - Swift Packages - Add Package Dependency… 菜单中,搜索

CoreStore

其中 JohnEstropia 是 *Owner* (fork 也可能会出现)。 然后添加到你的项目

变更集

有关完整的 Changelog,请参阅 Releases 页面。

联系方式

您可以在 Twitter 上联系我 @JohnEstropia

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

日语也支持,欢迎提问!

谁在使用 CoreStore?

我很乐意听到有关使用 CoreStore 的应用程序的信息。 请给我留言,我会欢迎任何反馈!

许可证

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