Core Data 的实用工具
Core Data 是一个强大而复杂的工具,但我一直觉得它不容易学习。 我已经大量使用它多年,但当我尝试使用它做一些新的事情时,我仍然会感到困惑。 RCDataKit 是我多年来制作的一系列辅助工具,旨在使一切变得更容易一些。
RCDataKit 正在开发中,并且我刻意保持它的简单性,以便初学者可以通过浏览源代码来从中学习。 如果您想要更强大的东西,请查看这些非常棒的库
当然。 Core Data 已经很老了,使用起来可能会很烦人,并且将来可能会被 SwiftData 永久取代。 但就目前而言,我仍然在我的项目中使用 Core Data,因为 SwiftData 无法像我希望的那样工作。 在它解决了很多错误之前,我将继续使用久经考验的工具,即使它有时令人沮丧且难以学习。
在您自己的 Package 中,将以下内容添加到您的依赖项中
dependencies: [
.package(url: "https://github.com/RCCoop/RCDataKit", .upToNextMajor(from: "0.1.0"))
]
或者使用 File -> Add Package Dependencies...
将包添加到您的 Xcode 项目中
这个简单的协议用于包装 NSPersistentContainer
并提供预配置的 NSManagedObjectContexts
的类型。
let myStack: DataStack
// Get the viewContext -- a NSManagedObjectContext where
// transactionAuthor == myStack.mainContextAuthor.name
let viewContext = myStack.viewContext
// Get a background context where transactionAuthor == TransactionAuthor.cloudDataImport.name
let bgContext = myStack.backgroundContext(author: .cloudDataImport)
// TransactionAuthor can also be initialized with a String literal
let anotherContext = myStack.backgroundContext(author: "otherContext")
这里有一些预制的 DataStack
实现
NSManagedObjectModel
并行运行测试用例通常会引发异常,因此最好串行而不是并发地运行这些测试。)一种简单的类型,用于跟踪持久存储中不同的上下文作者。 这本身没有任何作用,但在 DataStack
和 PersistentHistoryTracker
中用于标准化您的作者标题。
设置此项的简单方法是为 TransactionAuthor
创建一个扩展,以创建一个预设的作者列表--每个访问您的数据存储的主线程上下文(应用程序的视图上下文、小部件的上下文等)各一个,以及根据需要尽可能多的命名后台上下文,以跟踪谁或什么正在写入您的存储。
extension TransactionAuthor {
static var iOSViewContext: TransactionAuthor { "iOSViewContext" }
static var extensionContext: TransactionAuthor { "extensionContext" }
static var networkSync: TransactionAuthor { "networkSync" }
static var localEditing: TransactionAuthor { "localEditing" }
}
或者,您只需使用字符串字面量,就可以在调用站点为您 的TransactionAuthor
创建名称
let someContext = myStack.backgroundContext(author: "EditingTransaction")
这两个配对的协议(主要是 ModelFileManager
,它继承自 ModelManager
)是此库的几个其他部分所必需的,以便自动创建 NSManagedObjectModel
中的一些样板代码。 ModelFileManager
代表我们大多数人用于创建托管对象模型的 .xcdatamodeld
文件。
定义 ModelFileManager
很简单
enum TestModelFile: ModelFileManager {
static var bundle: Bundle {
.main // or use .module if your model file is in a separate module
}
static var modelName: String {
"TestModel"
}
static let model: NSManagedObjectModel = {
// Use one of RCDataKit's convenience methods to create the model:
NSManagedObjectModel.named(modelName, in: bundle)
}()
}
然后,您可以在 BasicDataStack
和 PreviewStack
的初始化器以及 ModelVersion
协议实现中使用您的 ModelFileManager
类型。
将您的模型从一个版本迁移到下一个版本可能非常痛苦——轻量级迁移很容易,但自定义迁移就没那么容易了。 但是现在我们有了 分阶段迁移! 不幸的是,Apple 的文档 缺乏。 感谢 Pol Piela 和 FatBobMan 来弥补不足。
使用 ModelVersion
协议,设置分阶段迁移需要更少的样板代码,因此您可以专注于执行迁移的重要工作。
ModelFileManager
类型enum Versions: String, ModelVersion {
// See `ModelFileManager` protocol:
typealias ModelFile = TestModelFile
// One case per version in your xcdatamodeld file:
case v1 = "Model"
case v2 = "Model2"
case v3 = "Model3"
case v4 = "Model4"
}
extension Versions {
static func migrationStages() -> [NSMigrationStage] {
[
v1.migrationStage(
toStage: .v2,
label: "Lightweight Migration: V1 to V2"),
v2.migrationStage(
toStage: .v3,
label: "Custom Migration: V2 to V3",
preMigration: { context in
// Do work before model is updated from v2 to v3
} postMigration: { context in
// Do work after model is updated
}),
v3.migrationStage(
toStage: .v4,
label: "Lightweight Migration: V3 to V4")
]
}
}
NSPersistentContainer
时,将 NSStagedMigrationManager
添加到描述选项let migrationManager = Versions.migrationManager()
container.persistentStoreDescriptions
.first?
.setOption(migrationManager, forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
或者,只需将您的 ModelVersion
传递到 BasicDataStack
的初始化器中
let stack = try BasicDataStack(
versionKey: Versions.self,
mainAuthor: .iOSViewContext)
持久历史记录跟踪有详细的文档记录,但仍然可能非常令人困惑。 PersistentHistoryTracker
是一个 actor,它附加到您的 NSPersistentContainer
以便管理所有跟踪。 它大量借鉴了 Antoine Van Der Lee 和 FatBobMan 的教程和项目(特别是 FatBobMan 的 PersistentHistoryTrackingKit,谢谢!),并根据 TransactionAuthor
添加了一些辅助程序。
开始跟踪
// Before loading your persistent store, set persistent history options:
let storeDescription = myPersistentContainer.persistentStoreDescriptions[0]
let trueOption = true as NSNumber
storeDescription.setOption(trueOption, forKey: NSPersistentHistoryTrackingKey)
storeDescription.setOption(trueOption, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
// Add a PersistentHistoryTracker to your container
self.tracker = PersistentHistoryTracker(
container: myPersistentContainer,
currentAuthor: .iOSViewContext)
// Start or stop monitoring as needed.
tracker.startMonitoring()
您还可以通过将 PersistentHistoryTrackingOptions
的实例传递给初始化器来在 BasicDataStack
中启用跟踪
let stack = try BasicDataStack(
versionKey: Versions.self,
mainAuthor: .iOSViewContext,
persistentHistoryOptions: .init())
stack.historyTracker?.startMonitoring()
您可以在单个调用中将 DataStack
及其 viewContext
添加到 SwiftUI 环境
struct MyView: View {
var myStack: MyDataStack
var body: some View {
SubView()
.dataStackEnvironment(myStack)
}
}
.dataStackEnvironment(_:)
调用包装了 DataStack 和 ManagedObjectContext 的 .environment(_:_:)
调用,因此您可以使用以下属性包装器访问任一环境值
struct SubView: View {
/// This is a `NSManagedObjectContext` accessed by `myStack.viewContext`
@Environment(\.managedObjectContext) var context
/// This is a `any DataStack` equal to `myStack` from `MyView`.
@EnvironmentDataStack var dataStack
var body: some View { ... }
}
您必须使用 dataStackEnvironment(_:)
调用将 DataStack 设置到视图的环境中,才能使用 @EnvironmentDataStack
属性包装器,否则会导致致命错误。
在任何您想要强制类型安全地围绕 ID 的地方,使用 TypedObjectID
代替 NSManagedObjectID
。 由于 NSManagedObjectID
和 TypedObjectID
包装器都是 Sendable
,因此它们是在上下文之间发送 NSManagedObject
引用的最佳方式。
let viewContextPerson = Person(...) // get a person on the ViewContext
let personId = TypedObjectID(viewContextPerson) // personId refers only to Person type
try backgroundContext.perform {
// get a reference to the same Person from storage, but safe for this context:
let backgroundContextPerson = try backgroundContext.existingObject(with: personId)
}
将 Updatable
协议添加到您的模型类型以获取一些免费函数。 协议一致性没有要求,除非实现类型是 NSManagedObject
子类。
let rc = Person(...)
rc.update(\.age, value: 15) // now I'm 15 years old!
rc.updateIfAvailable(\.age, value: nil) // still 15, not nil!
rc.update(\.age, value: 16, minimumChange: 2) // still 15, because I only want to age in 2-year increments.
rc.add(\.friend, relation: dan) // dan is now my friend
rc.add(\.friend, relation: nil) // nothing happens, because nobody's there.
rc.remove(\.friend, relation: dan) // dan's not my friend anymore.
为什么还要使用这些,而不是 rc.age = 15
或 rc.friend = dan
? 因为如果我已经 15 岁,或者如果 Dan 已经是我的朋友,使用 =
运算符仍然会导致 NSManagedObject.hasChanges
标志设置为 true
。 我喜欢确保如果某些东西没有改变,我可以相信 hasChanges
。
将大量数据导入 Core Data 不需要混乱。 只需在您要导入的数据类型中实现 Persistable
协议,该协议将引导您完成一些步骤,以确保一切都井然有序。
struct ImportablePerson {
var firstName: String
var lastName: String
var age: Int
var townID: Int
}
extension ImportablePerson: Persistable {
typealias ImporterData = [Int : Town]
// This function is called once per import operation to provide any extra
// necessary data for the import
static func generateImporterData(
objects: [Self],
context: NSManagedObjectContext
) throws -> ImporterData {
let townRefs = try context.getTownsWithIds(objects.map(\.townID))
return townRefs.reduce(into: [:]) { $0[$1] = $1.id }
}
// Then, for each item in the import operation, this function does the import:
func importIntoContext(
_ context: NSManagedObjectContext,
importerData: inout ImporterData
) -> PersistenceResult {
let persistedPerson = PersistentPerson(context: context)
persistedPerson.firstName = firstName
persistedPerson.lastName = lastName
persistedPerson.age = age
persistedPerson.town = importerData[townID]
return .insert(persistedPerson.objectID)
}
}
要使用导入器函数,只需使用 NSManagedObjectContext
上的便捷函数
let arrayResults: [PersistenceResult] = try context
.importPersistableObjects(importablePeople)
// results can also be a dictionary keyed to Identifiers.
let dictionaryResults: [ImportablePerson.ID : PersistenceResult] = try context
.importPersistableObjects(importablePeople)
NSManagedObjectContext
的扩展中有一些额外的函数可帮助进行基本操作
Updatable
协议函数更新对象属性,这将节省您不必要的 save()
调用。try context.saveIfNeeded()
NSManagedObjects
。let somePerson = try context.existing(Person.self, withID: personID)
let somePeople = try context.existing(Person.self, withIDs: [ID1, ID2, ID3])
NSPredicate
以仅删除符合给定条件的对象)。try context.removeInstances(of: Person.self, matching: someNSPredicate)
一点语法糖,用于使用链式函数构建您的 NSFetchRequest
let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
.sorted(sortDescriptors)
.where(somePredicate)
并用于构建 NSSortDescriptor
let sorting: [NSSortDescriptor] = [
.ascending(\Person.lastName),
.descending(\Person.age)
]
NSPredicate
可以与 &&
、||
和 !
运算符组合
let predicate1 = NSPredicate(format: "'age' >= 13")
let predicate2 = NSPredicate(format: "'age' < 20")
let isTeenager = predicate1 && predicate2
let isNotTeenager = !isTeenager
let alsoIsNotTeenager = !predicate1 || !predicate2
您还可以使用 NSManagedObject
子类上的 KeyPath
来创建简单的谓词
// Simple Equatable or Comparable KeyPaths allow this kind of NSPredicate creation
let isOlderThanDirt = \Person.age > 1000
let notFred = \Person.name != "Fred"
// Or wrap the KeyPath in parentheses for further NSPredicate functions:
let isTeenager = (\Person.age).between(13...19)
let isOddTeen = (\Person.age).in([11, 13, 15, 17, 19])
// String properties can have comparison options, too:
let definitelyNotFred = (\Person.name).notEqual(
to: "Fred",
options: .caseAndDiacriticInsensitive)
对于更强大、优雅和类型安全的 NSPredicate
系统,请查看 PredicateKit
RCDataKit 是一个正在进行中的项目……以下是我目前想到的一些总体改进
DataStack
协议中改进的多功能性由于它是一个正在进行中的项目,我很乐意接受建议、反馈或贡献。 如果您愿意,可以创建一个问题或拉取请求。
请浏览代码并随意使用它。 我希望它可以帮助您学习一些关于 Core Data 的知识,您可以在自己的项目中使用它们。 祝您愉快!