core-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Core Data

Core Data

Build and maintain data persistence using Core Data for apps that have not adopted SwiftData. Covers stack setup, concurrency, batch operations, NSFetchedResultsController, persistent history tracking, staged migration, and testing.
为未采用SwiftData的应用使用Core Data构建并维护数据持久化方案。内容涵盖栈配置、并发处理、批量操作、NSFetchedResultsController、持久化历史追踪、分阶段迁移及测试。

Contents

目录

Stack Setup

栈配置

NSPersistentContainer
encapsulates the Core Data stack.
swift
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()
    }
}
For CloudKit sync, use
NSPersistentCloudKitContainer
instead.
NSPersistentContainer
封装了Core Data栈。
swift
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

Concurrency and Threading

并发与线程

Core Data contexts are bound to queues. The
viewContext
is on the main queue; background contexts operate on private queues.
Rules:
  • Always use
    perform(_:)
    or
    performAndWait(_:)
    when accessing a context off its own queue.
  • Never pass
    NSManagedObject
    instances across context or thread boundaries. Pass
    NSManagedObjectID
    instead and re-fetch.
  • Set
    automaticallyMergesChangesFromParent = true
    on the
    viewContext
    .
swift
// 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()
    }
}
Core Data上下文与队列绑定。
viewContext
运行在主队列;后台上下文运行在私有队列。
规则:
  • 在上下文所属队列外访问时,务必使用
    perform(_:)
    performAndWait(_:)
  • 切勿跨上下文或线程传递
    NSManagedObject
    实例。应传递
    NSManagedObjectID
    并重新获取。
  • viewContext
    上设置
    automaticallyMergesChangesFromParent = true
swift
// 在后台上下文写入数据
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 Concurrency Integration

Swift Concurrency集成

NSManagedObjectContext.perform(_:)
has an
async throws
overload (iOS 15+). Avoid marking
NSManagedObject
subclasses as
Sendable
.
swift
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
}
Do not use
@unchecked Sendable
on managed objects.
If you need cross-boundary communication, pass the
objectID
(which is
Sendable
) and re-fetch:
swift
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()
    }
}
NSManagedObjectContext.perform(_:)
提供了
async throws
重载(iOS 15+)。请勿将
NSManagedObject
子类标记为
Sendable
swift
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()
    }
    // 保存完成后,若已配置,viewContext会自动合并更改
}
请勿在托管对象上使用
@unchecked Sendable
若需要跨边界通信,请传递
objectID
(它是
Sendable
)并重新获取:
swift
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

NSFetchedResultsController

Efficiently drives
UITableView
/
UICollectionView
from a Core Data fetch request, with built-in change tracking and optional caching.
swift
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)
    }
}
Key points:
  • The fetch request must have at least one sort descriptor.
  • Call
    deleteCache(withName:)
    before changing the fetch request predicate or sort descriptors, or set
    cacheName
    to
    nil
    .
  • The diffable snapshot delegate method (
    didChangeContentWith:
    ) is available iOS 13+ and is preferred over the older per-change callbacks.
  • After a context
    reset()
    , call
    performFetch()
    again.
高效地从Core Data请求结果驱动
UITableView
/
UICollectionView
,内置变更追踪与可选缓存功能。
swift
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 (可差分)

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

Batch Operations

批量操作

Batch operations execute at the SQL level, bypassing the managed object context. They are fast but don't trigger context notifications automatically.
批量操作在SQL层面执行,绕过托管对象上下文。速度快但不会自动触发上下文通知。

NSBatchInsertRequest (iOS 13+)

NSBatchInsertRequest (iOS 13+)

swift
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]
            )
        }
    }
}
swift
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 (iOS 9+)

swift
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]
            )
        }
    }
}
swift
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+)

NSBatchUpdateRequest (iOS 8+)

swift
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]
            )
        }
    }
}
Always merge changes back into relevant contexts after batch operations. Batch delete does not enforce the Deny delete rule.
swift
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]
            )
        }
    }
}
批量操作后务必将更改合并回相关上下文。批量删除不强制执行拒绝删除规则。

Persistent History Tracking

持久化历史追踪

Track store-level changes across targets (app, extensions, widgets) and processes.
跨目标(应用、扩展、小组件)和进程追踪存储级别的变更。

Enable History Tracking

启用历史追踪

swift
let description = NSPersistentStoreDescription()
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber,
    forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [description]
swift
let description = NSPersistentStoreDescription()
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber,
    forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [description]

Observe, Fetch, Merge, and Purge

监听、获取、合并与清理

swift
// 1. Observe remote change notifications
NotificationCenter.default.addObserver(
    self, selector: #selector(storeRemoteChange(_:)),
    name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator
)

// 2. Fetch history since last token
@objc func storeRemoteChange(_ notification: Notification) {
    let context = container.newBackgroundContext()
    context.perform {
        let request = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastToken)
        if let result = try? context.execute(request) as? NSPersistentHistoryResult,
           let transactions = result.result as? [NSPersistentHistoryTransaction] {
            // 3. Merge into viewContext
            for transaction in transactions {
                self.container.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
                self.lastToken = transaction.token
            }
        }
        // 4. Purge old history
        let purgeRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: self.lastToken)
        try? context.execute(purgeRequest)
    }
}
Store
lastToken
in
UserDefaults
(per target) so history is processed correctly across launches.
swift
// 1. 监听远程变更通知
NotificationCenter.default.addObserver(
    self, selector: #selector(storeRemoteChange(_:)),
    name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator
)

// 2. 获取上次token之后的历史记录
@objc func storeRemoteChange(_ notification: Notification) {
    let context = container.newBackgroundContext()
    context.perform {
        let request = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastToken)
        if let result = try? context.execute(request) as? NSPersistentHistoryResult,
           let transactions = result.result as? [NSPersistentHistoryTransaction] {
            // 3. 合并到viewContext
            for transaction in transactions {
                self.container.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
                self.lastToken = transaction.token
            }
        }
        // 4. 清理旧历史记录
        let purgeRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: self.lastToken)
        try? context.execute(purgeRequest)
    }
}
lastToken
存储在
UserDefaults
(按目标区分),确保跨启动能正确处理历史记录。

Staged Migration

分阶段迁移

NSStagedMigrationManager
(iOS 17+) sequences schema migrations through ordered stages, each lightweight or custom.
swift
import CoreData

// Define migration stages
// Use version checksums from the compiled model versions, not model names.
let checksumV1 = "<ModelV1 version checksum>"
let checksumV2 = "<ModelV2 version checksum>"
let stage1to2 = NSLightweightMigrationStage([checksumV1, checksumV2])
stage1to2.label = "Add isFavorite property"

let stage2to3 = NSCustomMigrationStage(
    migratingFrom: NSManagedObjectModelReference(
        named: "ModelV2", in: Bundle.main),
    to: NSManagedObjectModelReference(
        named: "ModelV3", in: Bundle.main)
)
stage2to3.label = "Split name into firstName/lastName"
stage2to3.willMigrateHandler = { migrationManager, currentStage in
    guard let container = migrationManager.container else { return }
    let context = container.newBackgroundContext()
    try context.performAndWait {
        // Transform data between schema versions
        let request = NSFetchRequest<NSManagedObject>(entityName: "Person")
        let people = try context.fetch(request)
        for person in people {
            let fullName = person.value(forKey: "name") as? String ?? ""
            let parts = fullName.split(separator: " ", maxSplits: 1)
            person.setValue(String(parts.first ?? ""), forKey: "firstName")
            person.setValue(parts.count > 1 ? String(parts.last!) : "", forKey: "lastName")
        }
        try context.save()
    }
}

// Apply to the persistent store
let manager = NSStagedMigrationManager([stage1to2, stage2to3])
let description = NSPersistentStoreDescription()
description.setOption(manager,
    forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
    if let error { fatalError("Migration failed: \(error)") }
}
For apps targeting below iOS 17, use lightweight migration (
NSInferMappingModelAutomaticallyOption
) or mapping models.
NSLightweightMigrationStage
takes version checksums (
[String]
), not human-readable model names.
NSStagedMigrationManager
(iOS 17+)按顺序执行分阶段schema迁移,每个阶段可选择轻量或自定义迁移。
swift
import CoreData

// 定义迁移阶段
// 使用编译后模型版本的校验和,而非模型名称。
let checksumV1 = "<ModelV1 version checksum>"
let checksumV2 = "<ModelV2 version checksum>"
let stage1to2 = NSLightweightMigrationStage([checksumV1, checksumV2])
stage1to2.label = "Add isFavorite property"

let stage2to3 = NSCustomMigrationStage(
    migratingFrom: NSManagedObjectModelReference(
        named: "ModelV2", in: Bundle.main),
    to: NSManagedObjectModelReference(
        named: "ModelV3", in: Bundle.main)
)
stage2to3.label = "Split name into firstName/lastName"
stage2to3.willMigrateHandler = { migrationManager, currentStage in
    guard let container = migrationManager.container else { return }
    let context = container.newBackgroundContext()
    try context.performAndWait {
        // 在schema版本间转换数据
        let request = NSFetchRequest<NSManagedObject>(entityName: "Person")
        let people = try context.fetch(request)
        for person in people {
            let fullName = person.value(forKey: "name") as? String ?? ""
            let parts = fullName.split(separator: " ", maxSplits: 1)
            person.setValue(String(parts.first ?? ""), forKey: "firstName")
            person.setValue(parts.count > 1 ? String(parts.last!) : "", forKey: "lastName")
        }
        try context.save()
    }
}

// 应用到持久化存储
let manager = NSStagedMigrationManager([stage1to2, stage2to3])
let description = NSPersistentStoreDescription()
description.setOption(manager,
    forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
    if let error { fatalError("Migration failed: \(error)") }
}
针对iOS 17以下版本的应用,使用轻量迁移(
NSInferMappingModelAutomaticallyOption
)或映射模型。
NSLightweightMigrationStage
接收版本校验和
[String]
),而非易读的模型名称。

Composite Attributes

复合属性

iOS 17+ supports composite attributes: groups of sub-attributes on an entity that act as a single logical unit. Define them in the model editor by adding a Composite type attribute and nesting sub-attributes beneath it.
Composite attributes map to
Codable
structs in SwiftData coexistence scenarios.
iOS 17+支持复合属性:实体上一组子属性作为单个逻辑单元。在模型编辑器中添加Composite类型属性并在其下嵌套子属性即可定义。
在SwiftData共存场景中,复合属性会映射到
Codable
结构体。

Testing

测试

In-Memory Store for Tests

测试用内存存储

swift
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")
    }
}
Tips:
  • Share the
    NSManagedObjectModel
    instance across tests to avoid "duplicate entity" warnings.
  • Use a single shared model loaded once:
swift
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
}
swift
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
    实例,避免“重复实体”警告。
  • 使用单次加载的共享模型:
swift
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)
    // ... 配置内存存储
}

Common Mistakes

常见错误

MistakeFix
Passing
NSManagedObject
across threads
Pass
objectID
and re-fetch in the target context
Forgetting to merge batch operation resultsCall
mergeChanges(fromRemoteContextSave:into:)
Calling
save()
without checking
hasChanges
Guard with
context.hasChanges
first
Using deprecated
init(concurrencyType:)
confinement type
Use
.privateQueueConcurrencyType
or
.mainQueueConcurrencyType
Not setting
mergePolicy
on
viewContext
Set
NSMergeByPropertyObjectTrumpMergePolicy
to avoid conflict crashes
Modifying fetch request on live
NSFetchedResultsController
without deleting cache
Call
deleteCache(withName:)
first or use
cacheName: nil
Batch delete ignoring Deny delete ruleBatch delete bypasses delete rules; validate manually
Marking
NSManagedObject
as
@unchecked Sendable
Do not. Pass
objectID
instead
错误修复方案
跨线程传递
NSManagedObject
传递
objectID
并在目标上下文重新获取
忘记合并批量操作结果调用
mergeChanges(fromRemoteContextSave:into:)
未检查
hasChanges
就调用
save()
先通过
context.hasChanges
判断
使用已废弃的
init(concurrencyType:)
限制类型
使用
.privateQueueConcurrencyType
.mainQueueConcurrencyType
未在
viewContext
上设置
mergePolicy
设置
NSMergeByPropertyObjectTrumpMergePolicy
避免冲突崩溃
修改已激活
NSFetchedResultsController
的请求但未删除缓存
先调用
deleteCache(withName:)
或设置
cacheName: nil
批量删除忽略拒绝删除规则批量删除绕过删除规则,需手动校验
NSManagedObject
标记为
@unchecked Sendable
请勿这么做,改为传递
objectID

Review Checklist

评审检查清单

  • NSPersistentContainer
    is initialized once and shared
  • viewContext
    used only on main queue; background contexts for writes
  • perform(_:)
    or
    performAndWait(_:)
    wraps all off-queue context access
  • automaticallyMergesChangesFromParent
    set on
    viewContext
  • mergePolicy
    set on
    viewContext
    to prevent conflict crashes
  • Batch operation results merged into relevant contexts
  • NSFetchedResultsController
    fetch requests have sort descriptors
  • Persistent history tracking enabled for multi-target apps
  • Tests use in-memory stores with shared
    NSManagedObjectModel
  • No
    NSManagedObject
    instances cross thread boundaries
  • NSPersistentContainer
    仅初始化一次并共享
  • viewContext
    仅在主队列使用;写入操作使用后台上下文
  • 所有队列外的上下文访问都用
    perform(_:)
    performAndWait(_:)
    包裹
  • viewContext
    已设置
    automaticallyMergesChangesFromParent
  • viewContext
    已设置
    mergePolicy
    防止冲突崩溃
  • 批量操作结果已合并到相关上下文
  • NSFetchedResultsController
    请求包含排序描述符
  • 多目标应用已启用持久化历史追踪
  • 测试使用内存存储并共享
    NSManagedObjectModel
  • NSManagedObject
    实例跨线程传递

References

参考资料