WWDC Quick Look 💓 By SwiftGGTeam
Bring Core Data concurrency to Swift and SwiftUI

Bring Core Data concurrency to Swift and SwiftUI

观看原视频

Highlight

Core Data 在 iOS 15 中全面拥抱 Swift 5.5 的并发模型,NSManagedObjectContext 新增了 perform 的 async 重载,支持 try await 返回值和抛出错误;SwiftUI 的 @FetchRequest 增加了动态配置和 @SectionedFetchRequest 分组查询能力,让数据驱动 UI 的代码更简洁、更安全。

核心内容

Core Data 为什么需要并发

持久化数据需要读写外部存储介质,这天然是异步操作。Core Data 从诞生之初就关注并发执行,但过去的 API(performAndWaitperform)需要开发者手动处理线程同步和结果传递。

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
        }
      }
    }
  }
}

关键点:

  • @FetchRequestsortDescriptors 现在是可变的
  • 通过 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 支持

除了 NSManagedObjectContextNSPersistentContainerNSPersistentStoreCoordinator 也添加了类似的 async API:

// NSPersistentContainer 的 async load
let container = NSPersistentContainer(name: "Earthquakes")
try await container.loadPersistentStores()

核心启发

1. 用 async/await 重构所有 Core Data 后台操作

  • 做什么:将现有的 performAndWait + 手动错误处理代码迁移到新的 async perform
  • 为什么值得做:代码量减少 50% 以上,编译器自动保证线程安全,错误处理更自然
  • 怎么开始:从数据导入/导出功能开始,逐步替换所有后台 context 操作

2. 在 SwiftUI 中实现可排序、可搜索的列表

  • 做什么:利用 @FetchRequest 的动态配置能力,为列表添加排序和搜索功能
  • 为什么值得做:无需手动管理 fetched results controller,代码更简洁,自动处理数据变化
  • 怎么开始:使用 quakes.sortDescriptorsquakes.nsPredicate 动态修改查询条件

3. 构建按日期分组的数据展示

  • 做什么:用 @SectionedFetchRequest 实现按日期、类别等属性分组的数据列表
  • 为什么值得做:一行代码替代过去几十行的 NSFetchedResultsController + section 计算逻辑
  • 怎么开始:在实体上添加 @objc 计算属性,传入 @SectionedFetchRequestsectionIdentifier

4. 建立类型安全的模型验证测试

  • 做什么:用新的 AttributeType 枚举编写单元测试,验证运行时模型与 Xcode 模型编辑器一致
  • 为什么值得做:在开发早期捕获模型变更导致的运行时问题
  • 怎么开始:为每个实体编写 assertAttribute 测试,检查属性名称和类型

5. 设计安全的跨上下文数据传递

  • 做什么:在 async perform 闭包中只返回 NSManagedObjectID 或字典,不返回 managed object
  • 为什么值得做:避免跨上下文访问导致的崩溃和数据不一致
  • 怎么开始:审查所有 async perform 的返回值,确保不返回 managed object 实例

关联 Session

评论

GitHub Issues · utterances