🛠️ RCDataKit 💾

GitHub

Core Data 的实用工具

目录

它是什么?

Core Data 是一个强大而复杂的工具,但我一直觉得它不容易学习。 我已经大量使用它多年,但当我尝试使用它做一些新的事情时,我仍然会感到困惑。 RCDataKit 是我多年来制作的一系列辅助工具,旨在使一切变得更容易一些。

RCDataKit 正在开发中,并且我刻意保持它的简单性,以便初学者可以通过浏览源代码来从中学习。 如果您想要更强大的东西,请查看这些非常棒的库

为什么不直接使用 SwiftData?

当然。 Core Data 已经很老了,使用起来可能会很烦人,并且将来可能会被 SwiftData 永久取代。 但就目前而言,我仍然在我的项目中使用 Core Data,因为 SwiftData 无法像我希望的那样工作。 在它解决了很多错误之前,我将继续使用久经考验的工具,即使它有时令人沮丧且难以学习。

安装

要求

Swift Package Manager

在您自己的 Package 中,将以下内容添加到您的依赖项中

dependencies: [
  .package(url: "https://github.com/RCCoop/RCDataKit", .upToNextMajor(from: "0.1.0"))
]

或者使用 File -> Add Package Dependencies... 将包添加到您的 Xcode 项目中

创建数据栈

DataStack 协议

这个简单的协议用于包装 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 实现

这里有一些预制的 DataStack 实现

辅助类型

TransactionAuthor

一种简单的类型,用于跟踪持久存储中不同的上下文作者。 这本身没有任何作用,但在 DataStackPersistentHistoryTracker 中用于标准化您的作者标题。

设置此项的简单方法是为 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")

ModelManager 和 ModelFileManager 协议

这两个配对的协议(主要是 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)
    }()
}

然后,您可以在 BasicDataStackPreviewStack 的初始化器以及 ModelVersion 协议实现中使用您的 ModelFileManager 类型。

ModelVersion 协议

将您的模型从一个版本迁移到下一个版本可能非常痛苦——轻量级迁移很容易,但自定义迁移就没那么容易了。 但是现在我们有了 分阶段迁移! 不幸的是,Apple 的文档 缺乏。 感谢 Pol PielaFatBobMan 来弥补不足。

使用 ModelVersion 协议,设置分阶段迁移需要更少的样板代码,因此您可以专注于执行迁移的重要工作。

  1. 创建一个符合协议的类型,让它引用您的模型版本的名称,并为其提供一个 ModelFileManager 类型
Screenshot_2024-09-15_at_10 24 58_AM
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"
}
  1. 创建一个迁移阶段数组来逐步升级您的版本
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")
        ]
    }
}
  1. 在创建您的 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

持久历史记录跟踪有详细的文档记录,但仍然可能非常令人困惑。 PersistentHistoryTracker 是一个 actor,它附加到您的 NSPersistentContainer 以便管理所有跟踪。 它大量借鉴了 Antoine Van Der LeeFatBobMan 的教程和项目(特别是 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()

SwiftUI 集成

您可以在单个调用中将 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 属性包装器,否则会导致致命错误。

CRUD 助手

类型化的 NSManagedObjectID

在任何您想要强制类型安全地围绕 ID 的地方,使用 TypedObjectID 代替 NSManagedObjectID。 由于 NSManagedObjectIDTypedObjectID 包装器都是 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 协议

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 = 15rc.friend = dan? 因为如果我已经 15 岁,或者如果 Dan 已经是我的朋友,使用 = 运算符仍然会导致 NSManagedObject.hasChanges 标志设置为 true。 我喜欢确保如果某些东西没有改变,我可以相信 hasChanges

Persistable 协议

将大量数据导入 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 助手

NSManagedObjectContext 的扩展中有一些额外的函数可帮助进行基本操作

try context.saveIfNeeded()
let somePerson = try context.existing(Person.self, withID: personID)
let somePeople = try context.existing(Person.self, withIDs: [ID1, ID2, ID3])
try context.removeInstances(of: Person.self, matching: someNSPredicate)

NSFetchRequest 助手

一点语法糖,用于使用链式函数构建您的 NSFetchRequest

let fetchRequest = NSFetchRequest<Person>(entityName: "Person")
    .sorted(sortDescriptors)
    .where(somePredicate)

并用于构建 NSSortDescriptor

let sorting: [NSSortDescriptor] = [
    .ascending(\Person.lastName),
    .descending(\Person.age)
]

NSPredicate 助手

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 是一个正在进行中的项目……以下是我目前想到的一些总体改进

贡献/反馈

由于它是一个正在进行中的项目,我很乐意接受建议、反馈或贡献。 如果您愿意,可以创建一个问题或拉取请求。

请浏览代码并随意使用它。 我希望它可以帮助您学习一些关于 Core Data 的知识,您可以在自己的项目中使用它们。 祝您愉快!