mcpField
C
core-data

core-data

github.com/dpearson2699数据与数据库1.2k2.7k 次安装13 周前更新
代码仓库

关于 core-data

面向尚未迁移到 SwiftData 的应用开发者的 Core Data 实践技能,涵盖持久化、并发、批处理和迁移。内容还包括跨目标历史记录、界面数据驱动及测试要点。

主要功能

  1. 01Core Data 栈与并发管理
  2. 02NSFetchedResultsController 数据驱动
  3. 03批量插入、更新和删除
  4. 04持久化历史跟踪
  5. 05分阶段模型迁移
  6. 06内存存储测试

适用场景

  1. 01为应用搭建 Core Data 持久化
  2. 02在后台上下文导入或更新数据
  3. 03同步扩展与应用中的存储变更
  4. 04规划模型迁移并测试持久化逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill core-data

安装后在 Claude Code 中输入/core-data确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
persistent-history.md
staged-migration.md

详细介绍

Core Data

为尚未采用 SwiftData 的应用构建和维护 Core Data 持久化功能。涵盖堆栈设置、并发、批量操作、NSFetchedResultsController、持久化历史跟踪、分阶段迁移和测试。

目录

堆栈设置

NSPersistentContainer 封装了 Core Data 堆栈。

文档:NSPersistentContainer

import CoreData

final class CoreDataStack: @unchecked Sendable {
    static let shared = CoreDataStack()

    let container: NSPersistentContainer

    private init() {
        container = NSPersistentContainer(name: "MyAppModel")
        container.loadPersistentStores { _, error in
            if let error { fatalError("Core Data store failed: \(error)") }
        }
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
    }

    var viewContext: NSManagedObjectContext { container.viewContext }

    func newBackgroundContext() -> NSManagedObjectContext {
        container.newBackgroundContext()
    }
}

如需与 CloudKit 同步,请改用 NSPersistentCloudKitContainer。

并发与线程

Core Data 上下文绑定到队列。viewContext 位于主队列;后台上下文则在私有队列上运行。

文档:NSManagedObjectContext

规则:

  • 在上下文所属队列之外访问上下文时,始终使用 perform(_:) 或 performAndWait(_:)。
  • 切勿跨上下文或线程边界传递 NSManagedObject 实例。应改为传递 NSManagedObjectID,然后重新获取对象。
  • 在 viewContext 上将 automaticallyMergesChangesFromParent = true。
// Writing on a background context
func updateTrip(id: NSManagedObjectID, newName: String) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        guard let trip = try context.existingObject(with: id) as? CDTrip else {
            throw PersistenceError.notFound
        }
        trip.name = newName
        try context.save()
    }
}
Swift 并发集成

NSManagedObjectContext.perform(_:) 提供 async throws 重载(iOS 15+)。避免将 NSManagedObject 子类标记为 Sendable。

func importItems(_ records: [ItemRecord]) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        for record in records {
            let item = CDItem(context: context)
            item.id = record.id
            item.title = record.title
        }
        try context.save()
    }
    // After save completes, viewContext auto-merges if configured
}

不要在托管对象上使用 @unchecked Sendable。 如需跨边界通信,请传递 objectID(它符合 Sendable),然后重新获取对象:

let objectID = trip.objectID  // Sendable
Task.detached {
    let bgContext = CoreDataStack.shared.newBackgroundContext()
    try await bgContext.perform {
        let trip = try bgContext.existingObject(with: objectID) as! CDTrip
        trip.isFavorite = true
        try bgContext.save()
    }
}

NSFetchedResultsController

通过 Core Data 获取请求高效驱动 UITableView / UICollectionView,并内置变更跟踪和可选缓存。

文档:NSFetchedResultsController

import CoreData
import UIKit

class TripsViewController: UITableViewController, NSFetchedResultsControllerDelegate {

    private lazy var fetchedResultsController: NSFetchedResultsController<CDTrip> = {
        let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
        request.sortDescriptors = [
            NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: false)
        ]
        request.fetchBatchSize = 20

        let controller = NSFetchedResultsController(
            fetchRequest: request,
            managedObjectContext: CoreDataStack.shared.viewContext,
            sectionNameKeyPath: nil,
            cacheName: "TripsCache"
        )
        controller.delegate = self
        return controller
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        try? fetchedResultsController.performFetch()
    }

    // MARK: - UITableViewDataSource

    override func numberOfSections(in tableView: UITableView) -> Int {
        fetchedResultsController.sections?.count ?? 0
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        fetchedResultsController.sections?[section].numberOfObjects ?? 0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TripCell", for: indexPath)
        let trip = fetchedResultsController.object(at: indexPath)
        cell.textLabel?.text = trip.name
        return cell
    }

    // MARK: - NSFetchedResultsControllerDelegate (diffable)

    func controller(
        _ controller: NSFetchedResultsController<any NSFetchRequestResult>,
        didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
    ) {
        let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

要点:

  • 获取请求必须至少包含一个排序描述符。
  • 更改获取请求谓词或排序描述符之前,调用 deleteCache(withName:),或将 cacheName 设为 nil。
  • 可差异化快照的代理方法(didChangeContentWith:)适用于 iOS 13+,且优先于较早的逐项变更回调。
  • 上下文调用 reset() 后,再次调用 performFetch()。

批量操作

批量操作在 SQL 层执行,绕过托管对象上下文。它们速度很快,但不会自动触发上下文通知。

NSBatchInsertRequest(iOS 13+)

文档:NSBatchInsertRequest

func batchImport(_ records: [[String: Any]]) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        let request = NSBatchInsertRequest(
            entity: CDTrip.entity(),
            objects: records
        )
        request.resultType = .objectIDs
        let result = try context.execute(request) as? NSBatchInsertResult
        if let ids = result?.result as? [NSManagedObjectID] {
            NSManagedObjectContext.mergeChanges(
                fromRemoteContextSave: [NSInsertedObjectsKey: ids],
                into: [CoreDataStack.shared.viewContext]
            )
        }
    }
}
NSBatchDeleteRequest(iOS 9+)

文档:NSBatchDeleteRequest

func deleteOldTrips(before cutoff: Date) async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CDTrip.fetchRequest()
        fetchRequest.predicate = NSPredicate(format: "endDate < %@", cutoff as NSDate)
        let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        request.resultType = .resultTypeObjectIDs
        let result = try context.execute(request) as? NSBatchDeleteResult
        if let ids = result?.result as? [NSManagedObjectID] {
            NSManagedObjectContext.mergeChanges(
                fromRemoteContextSave: [NSDeletedObjectsKey: ids],
                into: [CoreDataStack.shared.viewContext]
            )
        }
    }
}
NSBatchUpdateRequest(iOS 8+)
func markAllTripsAsNotFavorite() async throws {
    let context = CoreDataStack.shared.newBackgroundContext()
    try await context.perform {
        let request = NSBatchUpdateRequest(entity: CDTrip.entity())
        request.propertiesToUpdate = ["isFavorite": false]
        request.resultType = .updatedObjectIDsResultType
        let result = try context.execute(request) as? NSBatchUpdateResult
        if let ids = result?.result as? [NSManagedObjectID] {
            NSManagedObjectContext.mergeChanges(
                fromRemoteContextSave: [NSUpdatedObjectsKey: ids],
                into: [CoreDataStack.shared.viewContext]
            )
        }
    }
}

批量操作后,始终将变更合并回相关上下文。批量删除不会执行 Deny 删除规则。

对于破坏性或可重试的批量操作,请采用验证闭环:预先检查谓词和预期数量,使用对象 ID 结果类型执行操作,将 ID 合并到活动上下文中,重新获取对象,并断言后置条件。失败时,恢复到干净的测试夹具,或先证明操作具有幂等性再重试;切勿盲目重新运行只完成了一部分的批量操作。

持久化历史记录跟踪

跟踪应用、扩展、窗口小组件等目标以及进程之间的存储级更改。核心工作流如下:

文档:NSPersistentHistoryChangeRequest

  1. 在加载存储之前启用持久化历史记录和远程更改通知。
  2. 观察更改,并在目标的持久化令牌之后获取事务。
  3. 将事务通知合并到实时上下文中,然后持久化新令牌。
  4. 仅清除所有相关使用者都已处理的历史记录。

实现存储选项、观察器、令牌持久化、合并循环或清除策略时,加载 persistent-history.md。

分阶段迁移

NSStagedMigrationManager(iOS 17+)通过有序的轻量级或自定义阶段来执行架构迁移。阶段输入使用已编译的模型版本校验和,而非模型名称。支持 iOS 17 以下系统的应用需要采用轻量级迁移或映射模型路径。

文档:NSStagedMigrationManager

构建有序阶段、模型引用、自定义处理程序和持久化存储选项时,加载 staged-migration.md。

复合属性

iOS 17+ 支持复合属性:实体上的子属性组,可作为单个逻辑单元。在模型编辑器中添加一个 Composite 类型的属性,并在其下嵌套子属性,即可定义复合属性。

文档:NSCompositeAttributeDescription

在与 SwiftData 共存的场景中,复合属性会映射到 Swift 中的 Codable 结构体。

SwiftData 边界

对于 Core Data 与 SwiftData 共存或迁移的实现,请使用 swiftdata 技能。交接前,请保留以下 Core Data 边界:

  • 如果 SwiftData 需要共享或迁移 Core Data 数据,则必须指向现有的持久化存储 URL。
  • 共享的持久化数据必须确保 Core Data 模型与 SwiftData @Model 类中的实体名称、属性名称、类型和架构兼容。
  • 使用 SwiftData @Attribute(originalName:) 映射已重命名的持久化属性。

测试

用于测试的内存存储
import CoreData
import Testing

struct CoreDataTests {
    func makeTestContainer() throws -> NSPersistentContainer {
        let container = NSPersistentContainer(name: "MyAppModel")
        let description = NSPersistentStoreDescription()
        description.type = NSInMemoryStoreType
        container.persistentStoreDescriptions = [description]

        var loadError: Error?
        container.loadPersistentStores { _, error in loadError = error }
        if let loadError { throw loadError }
        return container
    }

    @Test func createAndFetchTrip() throws {
        let container = try makeTestContainer()
        let context = container.viewContext

        let trip = CDTrip(context: context)
        trip.name = "Test Trip"
        trip.startDate = .now
        try context.save()

        let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
        let trips = try context.fetch(request)
        #expect(trips.count == 1)
        #expect(trips.first?.name == "Test Trip")
    }
}

提示:

  • 在测试之间共享 NSManagedObjectModel 实例,避免出现“重复实体”警告。
  • 使用单个共享模型,并只加载一次:
private let sharedModel: NSManagedObjectModel = {
    let url = Bundle.main.url(forResource: "MyAppModel", withExtension: "momd")!
    return NSManagedObjectModel(contentsOf: url)!
}()

func makeTestContainer() throws -> NSPersistentContainer {
    let container = NSPersistentContainer(name: "MyAppModel",
                                          managedObjectModel: sharedModel)
    // ... configure in-memory store
}

常见错误

错误修正方法
在线程间传递 NSManagedObject传递 objectID,并在目标上下文中重新获取
忘记合并批量操作结果调用 mergeChanges(fromRemoteContextSave:into:)
未检查 hasChanges 就调用 save()先使用 context.hasChanges 进行保护
使用已弃用的 init(concurrencyType:) 并发类型使用 .privateQueueConcurrencyType 或 .mainQueueConcurrencyType
未为 viewContext 设置 mergePolicy设置 NSMergeByPropertyObjectTrumpMergePolicy,避免冲突导致崩溃
在实时 NSFetchedResultsController 上修改获取请求,却未删除缓存先调用 deleteCache(withName:),或使用 cacheName: nil
批量删除忽略 Deny 删除规则批量删除会绕过删除规则;请手动验证
将 NSManagedObject 标记为 @unchecked Sendable不要这样做。请改为传递 objectID
共存期间让 SwiftData 指向一个全新的存储当 SwiftData 需要共享或迁移 Core Data 数据时,请使用现有存储 URL 和兼容的架构

审查清单

  • NSPersistentContainer 只初始化一次并共享
  • 仅在主队列上使用 viewContext;写入时使用后台上下文
  • 所有离队列的上下文访问都由 perform(_:) 或 performAndWait(_:) 包裹
  • 在 viewContext 上设置 automaticallyMergesChangesFromParent
  • 在 viewContext 上设置 mergePolicy,以避免冲突导致崩溃
  • 将批量操作的结果合并到相关上下文中
  • NSFetchedResultsController 的获取请求具有排序描述符
  • 多目标应用启用持久化历史跟踪
  • Core Data 与 SwiftData 的交接保留存储 URL、架构兼容性、实体和属性名称,以及重命名映射
  • 测试使用共享 NSManagedObjectModel 的内存存储
  • NSManagedObject 实例不会跨线程边界传递

参考资料

评论

支持 Markdown