WWDC Quick Look 💓 By SwiftGGTeam
What's new in SwiftData

What's new in SwiftData

观看原视频

Highlight

SwiftData 新增三项能力:@Query 支持按属性分组(sectionBy),@Attribute(.codable) 可持久化第三方框架类型,ModelResultsObserverHistoryObserver 让非 SwiftUI 代码也能监听数据变化。

核心内容

列表分组不再需要手写字典

做过分组列表的开发者都经历过这个流程:先从 SwiftData 查出全部数据,再按某个属性手动分组成 Dictionary,最后把字典转成 Section 塞进 List。代码不短,而且每次数据变化都要重新算一遍分组。

00:36)Apple 在 @Query 上加了 sectionBy 参数。传一个 KeyPath 进去,SwiftData 直接在查询层完成分组。视图层只需要两层 ForEach:外层遍历 sections,内层遍历每个 section 里的数据。

第三方类型终于有了逃生舱

想在模型里存一个 MKMapItem.Identifier?以前 SwiftData 会直接抛 fatal error:“Class property within Persisted Struct/Enum is not supported”。因为 MKMapItem.Identifier 是 MapKit 的 class,你没源码给它加 @Model,SwiftData 也 inspect 不了它。

04:52)新加的 @Attribute(.codable) 让 SwiftData 把序列化委托给类型自己。只要类型遵循 Codable,SwiftData 就存它的编码结果,不再尝试生成 schema。

代价是有的:Codable 属性对 SwiftData 不透明,不能用于 Predicate 过滤,也不能排序。类型结构变了也不会触发迁移。Apple 明确说这是一个 “escape hatch”,只用于你不控制的第三方类型,自己写的模型还是应该用 @Model 或值类型。

数据监听走出 SwiftUI

@Query 只能在 SwiftUI 视图里用。如果你的 app 有一个独立的状态对象需要从 SwiftData 取数据并响应变化,以前只能写 workaround:藏一个看不见的 SwiftUI 视图,或者自己接 NotificationCenter

07:21ModelResultsObserver@Query 的能力拆了出来。它支持过滤、排序、分组,配合 Swift Observation 框架的 withContinuousObservation,任何 Swift 代码都能收到数据变化的通知。

09:30HistoryObserver 走得更远。SwiftData 每次保存都会记录历史事务(history transaction),包含改了什么、谁改的、以及一个唯一 token。HistoryObserver 监听这些事务,适合用来做服务端同步或者响应 app extension 的修改。你可以按作者过滤,避免把服务端拉下来的改动又推回去。

详细内容

Sectioned Fetching

00:01@QuerysectionBy 参数接收一个 KeyPath,按该属性的值把查询结果分组。 wrapped value 仍然是 [Trip],但可以通过下划线前缀访问底层的 Query 对象,拿到 sections 集合。

struct TripListView: View {
    @Query(sort: \Trip.startDate, sectionBy: \.destination)
    var trips: [Trip]

    var body: some View {
        List(selection: $selection) {
            ForEach(_trips.sections) { section in
                Section(section.id) {
                    ForEach(section) { trip in
                        TripListItem(trip: trip)
                    }
                }
            }
        }
    }
}

关键点:

  • sectionBy: \.destination 告诉 SwiftData 按 destination 属性分组
  • _trips 是属性包装器的底层实例,通过它访问 sections
  • 每个 sectionid 就是分组键的值,这里用作 Section 的 header
  • 内层 ForEach 遍历的是 section 本身,它是一个 RandomAccessCollection

Codable 属性

04:59@Attribute(.codable) 把持久化交给类型的 Codable 实现。SwiftData 不再为这类属性生成 schema,而是存编码后的数据。

import SwiftData

@Model class Trip {
    struct Location: Codable {
        var latitude: Double
        var longitude: Double
    }

    var name: String
    var destination: String
    var startDate: Date
    var endDate: Date
    var location: Location?

    @Attribute(.codable) var mapItemIdentifier: MKMapItem.Identifier?
}

关键点:

  • MKMapItem.Identifier 是 MapKit 的 class,SwiftData 无法自动 inspect
  • 它遵循 Codable,所以可以用 .codable 属性标记
  • SwiftData 存的是编码后的二进制/JSON,内部结构对它不透明
  • 不能用于 #Predicate 过滤或 SortDescriptor 排序
  • 类型结构变更不会触发自动迁移,需要保证前后兼容的编码逻辑

ModelResultsObserver

08:20ModelResultsObserver 在非 SwiftUI 代码中提供与 @Query 等价的能力:查询数据并持续监听变化。

@Observable @MainActor final class MapCameraController {
    private let observer: ModelResultsObserver<Trip>
    var bounds: MapCameraBounds?
    private var token: ObservationTracking.Token?

    init(modelContext: ModelContext) throws {
        observer = try ModelResultsObserver<Trip>(modelContext: modelContext)

        token = withContinuousObservation(options: [.didSet]) { [weak self] _ in
            self?.bounds = self?.calculateBounds(trips: observer.results)
        }
    }

    private func calculateBounds(trips: [Trip]) -> MapCameraBounds? { /* ... */ }
}

关键点:

  • ModelResultsObserver 初始化时需要 ModelContext
  • 支持 predicatesortsectionBy,API 与 @Query 一致
  • withContinuousObservation 返回的 token 必须被强引用,否则监听立即失效
  • 闭包内需要读取 observer.results,Observation 框架通过属性访问追踪依赖
  • [weak self] 避免循环引用

HistoryObserver

08:21HistoryObserver 监听 SwiftData 的持久化历史事务,适合服务端同步场景。

@SyncActor final class ServerSync {
    private let observer: HistoryObserver
    private var token: ObservationTracking.Token?

    func start() throws {
        observer = try HistoryObserver(authors: ["App"], modelContainer: modelContainer)
        token = withContinuousObservation(options: .didSet) { [weak self] _ in
            _ = self?.observer.eventCounter
            self?.processChanges()
        }
    }

    private func processChanges() {
        // 用 ModelContext.fetchHistory 获取事务,上传到服务器
    }
}

关键点:

  • authors 参数过滤事务来源,避免把服务端同步下来的改动又推回去
  • eventCounter 是唯一的可观察属性,新事务到来时递增
  • 闭包内必须访问 eventCounter,Observation 框架才能建立依赖关系
  • 实际处理时调用 ModelContext.fetchHistory 获取事务详情
  • 每个事务有唯一 token,可用于追踪已处理的变更

核心启发

1. 把数据层从视图层彻底抽离

以前 MVVM 架构下,ViewModel 想监听 SwiftData 变化只能绕路。现在用 ModelResultsObserver,ViewModel 自己持有数据监听,View 只负责渲染。做什么:重构现有项目,把 @Query 从 ViewModel 里移出去。为什么值得做:View 和数据的耦合降低,测试更容易。怎么开始:把 @Query 换成 ModelResultsObserver,用 withContinuousObservation 接收变化后通过 @Observable 通知 View。

2. 做一个实时协作的笔记 App

HistoryObserverauthors 过滤和事务 token 天然适合多端同步。做什么:本地编辑产生的事务标记为 "App",服务端推送的事务标记为 "Server"HistoryObserver 只监听 "App" 的变更并上传。为什么值得做:不需要自己实现变更追踪,SwiftData 已经帮你记好了。怎么开始:初始化 HistoryObserver 时传入 authors: ["App"],在 processChanges 里调用 fetchHistory 获取事务列表。

3. 在 SpriteKit/SceneKit 游戏里用 SwiftData 存进度

游戏通常不用 SwiftUI,以前 SwiftData 的实时监听能力用不上。做什么:用 ModelResultsObserver 监听玩家数据变化,动态调整游戏状态。为什么值得做:游戏架构不用为了用 SwiftData 而硬塞 SwiftUI。怎么开始:在游戏的 GameManager 里持有一个 ModelResultsObserver<PlayerData>,数据变化时更新游戏内变量。

4. 按日期分组的日程/待办列表

sectionBy 让按日期分组变得 trivial。做什么:日程 App 里按天分组显示事件。为什么值得做:以前要手动按 Calendar 分组,现在一行 sectionBy 搞定。怎么开始:@Query(sort: \.startDate, sectionBy: \.dateString),其中 dateString 是一个计算属性返回格式化后的日期字符串。

5. 在模型里直接存复杂配置对象

有些第三方 SDK 的配置类型遵循 Codable 但结构很复杂。做什么:用 @Attribute(.codable) 直接存进 SwiftData,不用手动拆成基础类型。为什么值得做:省掉一堆转换代码,模型更干净。怎么开始:确认类型遵循 Codable,加 @Attribute(.codable) 标记即可。

关联 Session

评论

GitHub Issues · utterances