Bring Core Data concurrency to Swift and SwiftUI
Swift & UI 高级 26m

将 Core Data 并发引入 Swift 和 SwiftUI

Bring Core Data concurrency to Swift and SwiftUI

2021年6月9日

在 Apple 官方观看视频

一句话判断

Core Data 终于正式拥抱 Swift Concurrency——@MainActor 标注的 View Context 和 NSManagedObjectContext 的 perform 方法现在可以用 async/await 写了,不用再嵌套回调地狱。

这场 Session 讲了什么

Core Data 的并发模型一直是 iOS 开发中最容易踩坑的部分之一。线程安全规则很严格:NSManagedObject 绑定到创建它的 NSManagedObjectContext,跨线程访问会直接 crash。iOS 15 中,Apple 为 Core Data 加入了原生 Swift Concurrency 支持,让并发代码的编写变得更安全。

Session 展示了如何用 @MainActor 标注 View Context,这样在 SwiftUI 视图中访问 Core Data 对象时,编译器会自动保证线程安全。对于后台操作,NSManagedObjectContext 新增了 performAndWaitperform 的 async 版本,配合 @Sendable 闭包,可以在 async 上下文中安全地执行数据库操作。

另外还讨论了 NSFetchedResultsController 在 SwiftUI 中的替代方案——@FetchRequest 配合 @SectionedFetchRequest 可以实现分组列表的数据绑定,性能和 UIKit 版本相当。

值得深挖的点

View Context 与 Background Context 的职责划分

很多开发者习惯在 View Context 上做所有操作,然后在性能出问题时才想起 Background Context。Session 明确了分工:View Context 只做 UI 读取,所有写入操作(创建、更新、删除)都应该在 Background Context 上完成。写入完成后,通过 mergeChangesFromContextDidSave 通知 View Context 刷新。

配合 Swift Concurrency,这个模式变得更清晰。Background Context 的操作可以放在一个 @Sendable 闭包中,用 context.perform { } 包裹,编译器会帮你检查是否有非法的跨 context 引用。

@FetchRequest 的性能陷阱

@FetchRequest 在 SwiftUI 视图中自动监听 Core Data 变化并触发 UI 更新。但如果你的 fetch 请求涉及大量数据(比如上千条记录),每次变更都会触发完整的 diff 计算。Session 建议对大数据集使用 batch 属性做分页,或者在 NSFetchRequest 上设置 fetchBatchSize 来限制内存占用。

代码片段

// 使用 @MainActor 标注的 Core Data Stack
import CoreData

@MainActor
class DataController: ObservableObject {
    let container: NSPersistentContainer
    var viewContext: NSManagedObjectContext {
        container.viewContext
    }
    
    init() {
        container = NSPersistentContainer(name: "MyApp")
        container.loadPersistentStores { _, error in
            if let error = error {
                fatalError("加载失败: \(error)")
            }
        }
    }
    
    // 在后台 context 中执行写入操作
    func saveNewTask(title: String) async throws {
        let bgContext = container.newBackgroundContext()
        try await bgContext.perform {
            let task = TaskItem(context: bgContext)
            task.title = title
            task.createdAt = Date()
            try bgContext.save()
        }
        // 保存后 viewContext 会自动合并变更
    }
}
// SwiftUI 中安全使用 @FetchRequest
import SwiftUI

struct TaskListView: View {
    // @FetchRequest 自动监听 viewContext 的变更
    @FetchRequest(
        sortDescriptors: [
            NSSortDescriptor(keyPath: \TaskItem.createdAt, ascending: false)
        ],
        animation: .default
    )
    private var tasks: FetchedResults<TaskItem>
    
    @Environment(\.managedObjectContext) private var viewContext
    
    var body: some View {
        List(tasks) { task in
            TaskRow(task: task)
        }
    }
}
// 使用 SectionedFetchRequest 实现分组列表
import SwiftUI

struct GroupedTaskView: View {
    @SectionedFetchRequest(
        sectionIdentifier: \TaskItem.category,
        sortDescriptors: [
            NSSortDescriptor(keyPath: \TaskItem.category, ascending: true),
            NSSortDescriptor(keyPath: \TaskItem.createdAt, ascending: false)
        ],
        animation: .default
    )
    private var sections: SectionedFetchResults<String?, TaskItem>
    
    var body: some View {
        List(sections) { section in
            Section(header: Text(section.id ?? "未分类")) {
                ForEach(section) { task in
                    TaskRow(task: task)
                }
            }
        }
    }
}

最佳实践

迁移到 Swift Concurrency 时,不要一次性改写所有 Core Data 代码。先从新写的功能开始用 async/await,旧的 completion-based 代码可以逐步迁移。两者的混合使用是安全的,因为 context.perform 的 async 版本内部只是对原有 performAndWait 的封装。

如果你使用 NSPersistentCloudKitContainer,注意 CloudKit 的同步操作会在后台 context 中执行。不要在 View Context 的 objectsWillChange 通知中做繁重的计算——这个通知可能在主线程上高频触发。

还有什么值得关注

  • NSManagedObjectContext 新增了 transactionAuthor 属性,可以标记数据变更的来源,方便调试。
  • NSPersistentHistoryToken 现在可以持久化到 UserDefaults,用来追踪「上次同步到哪一步」。
  • 配合 SwiftUI 的 @AppStorage 可以把一些轻量的用户偏好从 Core Data 中剥离出来,减少不必要的数据库查询。
WWDC 2021