Code-along: Add persistence with SwiftData
SwiftUI & UI Frameworks 进阶 22m

跟随编程:使用 SwiftData 添加持久化功能

Code-along: Add persistence with SwiftData

2026年6月10日

在 Apple 官方观看视频

一句话判断

这场 Session 最值得关注的一件事是:SwiftData 迁移绝不是无脑加 @Model 宏,它强迫你重构底层数据结构(比如把 Enum 拍平成 Class 继承),并且苹果终于用全新的 withContinuousObservation API 解决了 @ModeldidSet 失效的痛点。

这场 Session 讲了什么

这次苹果拿 Wishlist 这个纯内存的 SwiftUI 示例 App 开刀,演示怎么把它改造成带持久化的现代应用。以前这 App 靠一个 DataSource 类在内存里硬扛所有数据,一杀进程就灰飞烟灭。现在要把这些内存状态塞进 SwiftData 的 ModelContext(模型上下文)里。

过程其实挺折腾的。你不能只是给现有的 struct 或 enum 贴上 @Model 标签就下班。苹果在演示里硬生生把 Goal 这个枚举拆解成了 TripGoalActivityGoal 两个子类,搞起了类继承。视图层也全盘推翻,把环境里传的 DataSource 全换成了 @Query 宏和 FetchDescriptor(获取描述符),让视图直接跟数据库对话。

这对那些正打算把 @Observable 内存模型升级为本地持久化的团队影响最大。它直接打破了“SwiftData 就是 CoreData 的简单语法糖”的幻想,告诉你底层数据范式的转换是躲不掉的。

值得深挖的点

把 Enum 拍平成 Class 继承,SwiftData 的“对象图”执念

在 Swift 里,我们太喜欢用 Enum 带关联值来表示状态了,比如 Goal 原本是个枚举,用来区分是旅行目标还是活动目标。但在 SwiftData 的世界里,这套行不通。关系型数据库和对象图不认 Swift 的 Enum 关联值。

苹果给出的解法是使用模型继承(Model Inheritance)。把 Goal 变成基类,抽出 TripGoalActivityGoal 作为子类。

这个设计的 Trade-off 很明显:你失去了 Swift Enum 极其优雅的模式匹配(switch)和值语义,代码写起来会变得“Java 味”很重。但换来的好处是实打实的:数据库层面支持了多态,你可以直接用 #Predicate 去独立查询某种特定的 Goal,而不需要把整个大 Enum 反序列化到内存里再过滤。如果你以后的 App 需要重度依赖本地数据库,尽早放弃在持久化模型里用复杂 Enum 的念头,拥抱 Class 继承。

withContinuousObservation 终于救活了属性观察者

用过 @Model 宏的人都知道,它和 Swift 原生的 didSet 属性观察者(Property Observers)八字不合。以前你想在某个属性改变时自动更新 dateEdited,用 didSet 经常会遇到触发时机不对或者和计算属性冲突的坑。

这次 Session 提到了全新的 withContinuousObservation API。虽然演示代码没完全展开,但这个 API 的意图很明确:让 SwiftData 的模型变更追踪和 SwiftUI 的视图刷新机制在属性级别完美对齐。它允许你在模型内部安全地监听属性变化并做出副作用操作(比如更新时间戳),而不会打断 SwiftData 的自动保存(Autosave)机制。这是一个巨大的体验提升,以前为了绕开这个坑,大家不得不在 ViewModel 层写一堆胶水代码,现在终于可以收拢回 Model 层了。

代码片段

1. 在 init 中动态构建 @Query

场景:列表页需要根据外部传入的参数(如搜索词或分类)动态过滤数据,而不是写死查询条件。

import SwiftUI
import SwiftData

struct TripCollectionView: View {
    // 使用 @Query 宏声明数据源
    @Query private var trips: [Trip]
    
    let tripCollection: TripCollection
    
    // 在初始化器中动态修改 _trips 的查询条件
    init(tripCollection: TripCollection) {
        self.tripCollection = tripCollection
        
        // 动态构建带 #Predicate 的 Query
        _trips = Query(
            filter: #Predicate<Trip> { $0.collection == tripCollection }, 
            sort: \Trip.name
        )
    }
    
    var body: some View {
        List(trips) { trip in
            Text(trip.name)
        }
    }
}

坑:在 init 里赋值 _trips 时,千万别漏了下划线,否则你只是在给一个普通的计算属性赋值,SwiftUI 根本不会触发数据库查询。

2. 使用 @Relationship 处理级联删除与反向关联

场景:一个 Trip 包含多个 Activity,当删除 Trip 时,需要自动清理关联的 Activity,避免数据库里产生孤儿数据。

import Foundation
import SwiftData

@Model
class Trip {
    var name: String
    
    // 声明一对多关系,设置级联删除规则,并指定反向关联属性
    @Relationship(deleteRule: .cascade, inverse: \Activity.trip)
    var activities: [Activity] = []
    
    // 内联缩略图数据,避免只存文件路径导致文件移动后失效
    var thumbnailData: Data? 
    
    init(name: String) {
        self.name = name
    }
}

@Model
class Activity {
    var name: String
    // 反向关联属性,SwiftData 会自动维护两端的同步
    var trip: Trip? 
}

坑:如果只写了 @Relationship 没写 inverse,SwiftData 会尝试自己推断,但在复杂模型下极易推断失败导致编译报错或运行时崩溃,永远显式写出 inverse

最佳实践

  • 已有项目的迁移策略:别想着一步到位。先剥离内存 DataSource,把纯数据模型改成 @Model。遇到 Enum 状态机,果断重构为 Class 继承体系。把环境对象(EnvironmentObject)替换为 @Query 时,按视图层级逐个替换,确保每次替换后 UI 渲染正常。
  • 新项目的采用建议:直接在 Model 层设计好继承体系,别用 Enum 凑合。对于需要频繁查询的字段,确保它们是可被 #Predicate 解析的基础类型。
  • 实战中容易踩的坑@Model 里的属性如果是自定义类型(比如示例中的 TripCollection 枚举),必须显式实现 Codable 协议,否则 SwiftData 无法将其序列化到数据库列中,会直接抛出编译错误。

还有什么值得关注

  • 内联缩略图数据:示例中将 thumbnailData: Data? 直接存入数据库,而不是只存文件路径。这避免了文件被系统清理或移动导致路径失效的问题,对于小体积图片来说,读取效率反而更高。
  • FetchDescriptorfetchLimit:在首页展示“最近旅行”时,使用了 fetchLimit: 5。这是一个极其好用的内存保护机制,直接限制底层 SQL 拉取的条数,避免一次性把几千条数据全加载到内存里。
  • 运行时错误暴露:Session 提到了使用 SwiftUI view modifiers 来捕获和展示 SwiftData 的运行时错误(如数据库损坏或 Schema 迁移失败),这在以前只能靠 do-catch 盲猜,现在有了更优雅的 UI 降级方案。
SwiftData SwiftUI Data Modeling @Query Persistence