Highlight
Core Data 在 iOS 15 中全面拥抱 Swift 5.5 的并发模型,
NSManagedObjectContext新增了perform的 async 重载,支持try await返回值和抛出错误;SwiftUI 的@FetchRequest增加了动态配置和@SectionedFetchRequest分组查询能力,让数据驱动 UI 的代码更简洁、更安全。
核心内容
Core Data 为什么需要并发
持久化数据需要读写外部存储介质,这天然是异步操作。Core Data 从诞生之初就关注并发执行,但过去的 API(performAndWait、perform)需要开发者手动处理线程同步和结果传递。
Swift 5.5 引入了结构化并发模型,Core Data 顺势提供了更自然的 API。
旧方式的痛点
使用 performAndWait 时,调用线程会阻塞直到闭包执行完成。如果不需要等待结果,可以用 perform,但需要手动通过 completion handler 传递结果和错误。
// 过去:手动传递错误
var fetchError: Error?
context.performAndWait {
do {
// ... fetch ...
} catch {
fetchError = error
}
}
if let error = fetchError {
// handle error
}
这种写法有几个问题:
- 需要声明可变变量来捕获结果
- 错误处理分散在多个位置
- 代码难以阅读和测试
新的 async perform API
iOS 15 中,NSManagedObjectContext 新增了 perform 的 async 重载:
let count = try await context.perform {
let request = Quake.fetchRequest()
let fiveHoursAgo = Calendar.current.date(byAdding: .hour, value: -5, to: Date())!
request.predicate = NSPredicate(format: "time >= %@", fiveHoursAgo as NSDate)
request.resultType = .countResultType
return try context.count(for: request)
}
关键点:
perform闭包可以throw错误,调用方用try await接收- 闭包可以
return值,直接传递到调用方 - 编译器自动处理线程切换和结果路由
- 无需手动声明捕获变量
调度策略:immediate vs enqueued
新的 async perform 支持两种调度策略:
.immediate(默认):
- 如果从不同执行上下文调用,会等待调度并执行
- 如果已在同一上下文,乐观地立即执行
.enqueued:
- 总是将工作追加到上下文工作队列末尾
- 无论调用方在哪个上下文
// 使用 enqueued 策略
let result = try await context.perform(schedule: .enqueued) {
// 这段代码总是排在当前队列末尾执行
return try context.count(for: request)
}
安全返回值的边界
新的 API 让返回值变得容易,但有一个重要限制:不能跨上下文返回已注册的 NSManagedObject。
// 危险:返回 managed object 到另一个上下文
let quake = try await backgroundContext.perform {
let request = Quake.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "time", ascending: false)]
request.fetchLimit = 1
return try backgroundContext.fetch(request).first // 不要这样做!
}
正确的做法:
- 使用
NSManagedObjectID,在目标上下文中重新获取 - 使用 fetch request 的 dictionary 结果类型
// 安全:返回 object ID
let objectID = try await backgroundContext.perform {
let request = Quake.fetchRequest()
request.resultType = .managedObjectIDResultType
request.fetchLimit = 1
return try backgroundContext.fetch(request).first as? NSManagedObjectID
}
// 在 view context 中重新获取
if let objectID = objectID {
let quake = viewContext.object(with: objectID)
}
重构导入流程
Earthquakes 示例展示了如何用 async/await 重构数据导入:
func importQuakes(from data: Data) async throws {
let quakes = try JSONDecoder().decode([Quake].self, from: data)
try await backgroundContext.perform {
for quake in quakes {
let newQuake = Quake(context: backgroundContext)
newQuake.magnitude = quake.magnitude
newQuake.place = quake.place
newQuake.time = quake.time
}
try backgroundContext.save()
}
}
关键点:
- 整个导入流程用
async throws声明 perform内部处理所有 Core Data 操作- 错误自然向上传播
- 调用方用
try await等待完成
Swift 化的 API 改进
Core Data 在 iOS 15 中对 Swift 开发者更友好了:
Persistent store 类型
// 新的 Swift 风格名称
let storeType = NSPersistentStore.StoreType.sqlite
// 替代旧的字符串 "SQLite"
Attribute 类型
// 新的 AttributeType 枚举
func assertAttribute(named name: String, on entity: NSEntityDescription, hasType type: NSAttributeDescription.AttributeType) {
guard let attribute = entity.attributesByName[name] else {
XCTFail("No attribute named \(name)")
return
}
XCTAssertEqual(attribute.type, type)
}
// 使用示例
assertAttribute(named: "magnitude", on: quakeEntity, hasType: .double)
assertAttribute(named: "place", on: quakeEntity, hasType: .string)
SwiftUI 中的 FetchRequest 增强
动态排序
// [20:36](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1236)
private let sorts = [(
name: "Time",
descriptors: [SortDescriptor(\Quake.time, order: .reverse)]
), (
name: "Time",
descriptors: [SortDescriptor(\Quake.time, order: .forward)]
), (
name: "Magnitude",
descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)]
), (
name: "Magnitude",
descriptors: [SortDescriptor(\Quake.magnitude, order: .forward)]
)]
struct ContentView: View {
@FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
private var quakes: FetchedResults<Quake>
@State private var selectedSort = SelectedSort()
var body: some View {
List(quakes) { quake in
QuakeRow(quake: quake)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
SortMenu(selection: $selectedSort)
.onChange(of: selectedSort) { _ in
let sortBy = sorts[selectedSort.index]
quakes.sortDescriptors = sortBy.descriptors
}
}
}
}
}
关键点:
@FetchRequest的sortDescriptors现在是可变的- 通过
quakes.sortDescriptors = ...动态切换排序 - 使用 Swift 的
SortDescriptor类型,类型安全
动态谓词
// [21:33](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1293)
struct ContentView: View {
@FetchRequest(sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
private var quakes: FetchedResults<Quake>
@State private var searchText = ""
var query: Binding<String> {
Binding {
searchText
} set: { newValue in
searchText = newValue
quakes.nsPredicate = newValue.isEmpty
? nil
: NSPredicate(format: "place CONTAINS %@", newValue)
}
}
var body: some View {
List(quakes) { quake in
QuakeRow(quake: quake)
}
.searchable(text: query)
}
}
关键点:
quakes.nsPredicate可以动态修改- 配合
.searchable修饰符实现搜索 - 空搜索时清除谓词,显示全部数据
分组查询 SectionedFetchRequest
// [23:26](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1406)
extension Quake {
lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d, yyyy"
return formatter
}()
@objc var day: String {
return dateFormatter.string(from: time)
}
}
struct ContentView: View {
@SectionedFetchRequest(
sectionIdentifier: \.day,
sortDescriptors: [SortDescriptor(\Quake.time, order: .reverse)])
private var quakes: SectionedFetchResults<String, Quake>
var body: some View {
List {
ForEach(quakes) { section in
Section(header: Text(section.id)) {
ForEach(section) { quake in
QuakeRow(quake: quake)
}
}
}
}
}
}
关键点:
@SectionedFetchRequest按指定属性自动分组sectionIdentifier指向用于分组的 key path- 返回
SectionedFetchResults,支持ForEach遍历 - 分组属性需要标记
@objc,因为 Core Data 依赖 KVO
动态分组切换
// [24:56](https://developer.apple.com/videos/play/wwdc2021/10017/?time=1496)
private let sorts = [(
name: "Time",
descriptors: [SortDescriptor(\Quake.time, order: .reverse)],
section: \Quake.day
), (
name: "Magnitude",
descriptors: [SortDescriptor(\Quake.magnitude, order: .reverse)],
section: \Quake.magnitude_str
)]
// 切换排序和分组
.onChange(of: selectedSort) { _ in
let sortBy = sorts[selectedSort.index]
let config = quakes
config.sectionIdentifier = sortBy.section
config.sortDescriptors = sortBy.descriptors
}
关键点:
sectionIdentifier可以动态修改- 切换分组时,整个列表自动重新组织
- 配合动画修饰符可获得平滑过渡效果
详细内容
调试工具
Sanitizers
Xcode 提供的 Address Sanitizer 和 Thread Sanitizer 对检测并发问题很有帮助:
- Address Sanitizer:检测内存使用问题
- Thread Sanitizer:检测多线程数据访问问题
在 Scheme Editor 的 Run 设置中启用。
Core Data 并发调试
启用特殊的运行时标志,Core Data 会验证内部锁和类型的正确使用:
// 在启动参数中添加
-com.apple.CoreData.ConcurrencyDebug 1
NSPersistentContainer 和 NSPersistentStoreCoordinator 的 async 支持
除了 NSManagedObjectContext,NSPersistentContainer 和 NSPersistentStoreCoordinator 也添加了类似的 async API:
// NSPersistentContainer 的 async load
let container = NSPersistentContainer(name: "Earthquakes")
try await container.loadPersistentStores()
核心启发
1. 用 async/await 重构所有 Core Data 后台操作
- 做什么:将现有的
performAndWait+ 手动错误处理代码迁移到新的 asyncperform - 为什么值得做:代码量减少 50% 以上,编译器自动保证线程安全,错误处理更自然
- 怎么开始:从数据导入/导出功能开始,逐步替换所有后台 context 操作
2. 在 SwiftUI 中实现可排序、可搜索的列表
- 做什么:利用
@FetchRequest的动态配置能力,为列表添加排序和搜索功能 - 为什么值得做:无需手动管理 fetched results controller,代码更简洁,自动处理数据变化
- 怎么开始:使用
quakes.sortDescriptors和quakes.nsPredicate动态修改查询条件
3. 构建按日期分组的数据展示
- 做什么:用
@SectionedFetchRequest实现按日期、类别等属性分组的数据列表 - 为什么值得做:一行代码替代过去几十行的
NSFetchedResultsController+ section 计算逻辑 - 怎么开始:在实体上添加
@objc计算属性,传入@SectionedFetchRequest的sectionIdentifier
4. 建立类型安全的模型验证测试
- 做什么:用新的
AttributeType枚举编写单元测试,验证运行时模型与 Xcode 模型编辑器一致 - 为什么值得做:在开发早期捕获模型变更导致的运行时问题
- 怎么开始:为每个实体编写
assertAttribute测试,检查属性名称和类型
5. 设计安全的跨上下文数据传递
- 做什么:在 async perform 闭包中只返回
NSManagedObjectID或字典,不返回 managed object - 为什么值得做:避免跨上下文访问导致的崩溃和数据不一致
- 怎么开始:审查所有 async perform 的返回值,确保不返回 managed object 实例
关联 Session
- Discover concurrency in SwiftUI — SwiftUI 中并发特性的具体使用方式,包括
task和refreshable - Explore structured concurrency in Swift — Swift 结构化并发的深入讲解,理解 Task、Actor 等概念
- What’s new in SwiftUI — SwiftUI 的新特性 overview,包括
@FetchRequest和@SectionedFetchRequest的更多用法
评论
GitHub Issues · utterances