
-
SwiftData:深入了解继承和架构迁移
了解如何使用类继承对数据进行建模。了解如何优化查询,并无缝迁移你的 App 数据以便使用继承。探索如何通过子类归类来构建模型图形、编写高效的数据获取和查询,并实现顺畅平稳的架构迁移。了解如何使用可观测持久性历史记录来有效进行更改跟踪。
章节
资源
相关视频
WWDC25
WWDC24
-
搜索此视频…
你好 我是 Rishi Verma SwiftData 团队的一名工程师 欢迎观看 “SwiftData:深入了解继承 和架构迁移” SwiftData 随 iOS 17 推出 支持在 Apple 全平台使用 Swift 进行 App 数据建模与持久化存储 通过运用现代 Swift 语言特性 它能帮助你编写快速、 高效且安全的代码 本期视频我们将重点探讨 如何运用类继承以及 何时采用继承才是正确的选择 在引入继承机制并完成架构演进后 我们讨论了用于保留数据的迁移策略 然后我们将探索几种 优化 SwiftData 数据获取 与查询性能的方法 最后简要说明如何 监测本地与远程的模型变更 过去几个版本中 我们一直以 “SampleTrips”这款 App 为例 这是一个基于 SwiftUI 开发的 App 用于追踪我规划的所有旅程 要将 SwiftData 与 这个 App 中的模型搭配使用 我只需导入框架
并用 Model 宏修饰每个模型
在 App 定义的 WindowGroup 上 添加 modelContainer 修饰符 即可向整个视图层级 声明 Trip 模型 完成 modelContainer 配置后 现在可以通过 Query 宏更新视图 我们将移除静态数据 转而使用 Query 宏 动态填充视图 这个宏会自动生成 从模型容器获取行程数据的代码
这就大功告成了 现在 这款 App 已可以持久存储 我创建的所有行程数据 并与 SwiftUI 视图完美融合 SwiftData 不仅让数据持久化 变得轻而易举 更提供了模型与架构迁移 关系图管理 CloudKit 同步等强大功能 而 SwiftData 最新引入的特性 正是类继承 iOS 26 新增了构建支持继承机制的 模型关系图能力 类继承是一个强大的工具 接下来我们就来探讨 何时适合使用这项工具 当模型之间存在自然层级关系 且共享特性时 继承机制就能发挥最大价值 Trip 模型有 destination、 startDate 和 endDate 这些所有行程都必备的属性 让我们明确行程的时空信息 因此 任何继承自 Trip 的新子类 都会自动拥有这些属性 以及 Trip 中定义的其他共享行为 Trip 模型本身也是一个 宽泛的领域概念 我们生活中的行程类型多种多样 Trip 的新子类应属于自然子域 且契合于更广泛的 Trip 主领域范畴
在我们的“SampleTrips”App 中 许多行程自然分为 两个子域:个人行程和商务行程 通过这两个表达行程 自然子域的新模型 我想添加这些子类特有的属性和行为 对于个人行程 我将添加一个枚举 来记录这次旅行的具体原因 而对于商务行程 我想添加一个每日津贴属性 以便了解下次出差应该花费多少 让我们在“SampleTrips”App 中 实现这个功能 打造更丰富的体验 这是我们想要与子类 共享属性的 Trip 类 现在让我们为“Trips”App 添加 两个新的子类 一个用于商务旅行 一个用于个人旅行 同时我需要用 iOS 26 上的 @available 来装饰它们 以确保与 SwiftData 的继承支持 保持一致 接下来让我们为子域 添加特定领域的属性 我可以为 BusinessTrip 添加 每日津贴并设置初始值 对于 PersonalTrip 我们添加一个 Reason 枚举 来记录个人旅行的原因 哦等等 最后还需要更新我们的架构 以包含新的子类 将 BusinessTrip 和 PersonalTrip 添加到 modelContainer 修饰符 这样就大功告成了 “SampleTrips”App 现已准备就绪 可以直接使用蓝色标识的个人旅行 和绿色标识的商务旅行 无需编写其他特殊代码 虽然类继承是一项强大的工具 但它并非适用于所有场景 现在来谈谈何时应该使用继承
在以下几种情况下 继承可能是正确的选择 当你的数据模型天然呈现层级关系 并且具有你想要扩展的共同特性时 继承就是合适的选择 因为你的类型形成了“is-a”关系
当我们使用继承的模型时 我们知道个人旅行是一种 IS-A 旅行 这意味着当我在视图里处理 Trip 类型时 比如这里的旅行查询 我可以预期会找到所有类型的旅行 包括个人旅行、商务旅行 以及父类 Trip 本身的实例 我们的旅行数据以飞机图标的 形式展示 颜色与 UI 配色一致 象征着它们从模型容器 飞向支撑查询的模型上下文 但是继承不应该被简单地用作 在模型之间共享共同特性的工具 例如 如果我们仅为所有 包含 name 属性的模型创建子类 就会导致类层次结构中 混杂多个子域 这些子域仅仅为了 共享一个共同属性而存在 而其他所有特性都局限于各自的子域 并且由于这些子域 并未形成自然的层级关系 采用协议遵从性 会是更合适的表达方式 协议遵从性允许不同域共享特定行为 而非其他无关特性 是否采用继承还需考虑另一个 关键因素 模型的查询与获取方式
数据查询有多种方式 当前我们使用 Query 宏 从模型容器中获取所有行程数据 来驱动视图 这属于深度查询的一个示例
若仅采用深度查询 即始终获取所有行程数据 且仅使用 Trip 类型 则应将 个人行程或商务行程作为 Trip 的属性而非子类来实现 反之 若查询或获取操作 仅针对最终子类类型 即浅层查询 则会考虑扁平化模型结构 因为此时 Trip 永远不会被直接查询或使用 但如果需要同时使用深度查询和 浅层查询 继承机制就能发挥作用 因为你既需要查询所有行程 也需要检索特定子类型 如 PersonalTrip 来驱动专属视图 现在 我们来看看 如何更新“Trips”App 让它仅显示个人行程或商务行程
我们可以使用分段控件 切换显示所有行程 或特定子类行程
所选分段控件的状态可用于生成谓词 通过 is 关键字判断数据 是否属于特定类型 例如 我检查它是否为 PersonalTrip 接着我们传入这个谓词 并按行程的 startDate 排序 来初始化查询 现在让我们在 App 中实际验证一下 初始显示为行程总览视图 因此我可以看到所有 Trip 类型数据 之后可以通过分段控件 精准筛选特定子类 完美! 这就是在 iOS 26 中 运用类继承的正确姿势 不过 我们还需要进一步完善 刚才对架构进行了重大修改 现在必须考虑 这些变更对现有 App 的影响 以及数据迁移方案 “SampleTrips”App 在过去的 多个版本迭代中 已经历数次演进 现在我们需要将这些变更记录到 版本化架构中 并制定架构迁移计划 以确保用户升级到最新版 “SampleTrips”App 时 数据能够完整保留
这一切始于我们的首个视频 当时 我们在 iOS 17 中引入 SwiftData 并以“Trips”为例演示用法 通过这些入门视频 我们学会了 如何确保行程名称的唯一性 以及如何通过修改属性原始名称 来实现数据迁移兼容
针对 iOS 17 我们构建了 版本号为 2.0 的架构 并展示了更新后的 Trip 模型 现在具有唯一名称 以及重命名的开始和结束日期
接着我们添加了自定义迁移阶段 专门处理现有“Trips”数据的 去重操作 这里运用了 ModelContext 的 fetch 函数获取全部行程记录 为后续去重处理做好准备
在 iOS 18 中 我们运用了 Index 和 Unique 宏 同时标记了希望在删除时保留的属性
这使得我们能够识别 已从数据存储中删除的模型
在 iOS 18 的版本化架构中 我们将它标记为版本 3 并记录 Trip 模型的变更 新增 Unique 和 Index 宏的使用 确保数据去重 并优化查询和获取性能 同时为这些属性添加了 删除时保留值的修饰符 以便在处理持久化历史记录时 仍能追踪到已删除的行程
在从版本 2 迁移至版本 3 时 我们还增加了另一个自定义迁移阶段 来再次对行程数据进行去重处理 如今在 iOS 26 中 我们将通过 子类化和轻量级迁移阶段 来创建版本 4 在当前的版本架构中 我们将它标记为版本 4 并列出架构中的所有模型 及新增的子类 由于这些子类都标注了 iOS 26 或更高版本要求 版本架构也遵循相同限制 我们还需要添加一个 从版本 3 到版本 4 的 轻量级迁移阶段 版本 4 的可用性与之前保持一致 随着最终版本架构 和迁移阶段构建完成 我们可以将这些内容统一封装成 从而明确定义版本架构的 执行顺序和迁移阶段 这一版本架构包含 按发布顺序排列的架构数组 当 iOS 26 发布时 我们会添加 包含子类的最新架构 然后建立一个迁移阶段数组 从而逐个版本地完成数据迁移 这就是我们构建架构迁移计划的方式 现在 我们已经构建了版本架构及 相应的架构迁移计划 下一步就是在创建“SampleTrips” 的模型容器时 使用它们 让我们回到 modelContainer 修饰符并更新它 以使用带有架构迁移计划的模型容器 首先向应用程序添加新的容器属性 我们将在那构建版本号为 4 的架构 将架构迁移计划提供给 ModelContainer 构造器 接着更新 modelContainer 修饰符 以使用新的可迁移容器 至此 我们已经确保“SampleTrips” 为支持继承特性所做的更新 能够无缝兼容之前发布的各个版本 同时完全保留用户数据 既然迁移问题已解决 现在该考虑优化驱动界面 和迁移阶段的查询和获取操作了 当前我们通过 Predicate 实现了 基于选定分段的查询 但在之前的视频中 我们还实现过搜索栏功能 现在让我们重新加入这个功能 并直接处理用户输入的搜索文本
我们先根据提供的 searchText 构建谓词 第一步 检查文本是否为空 若非空 则构建一个复合谓词 用于检查行程名称 或目的地是否包含给定文本 接着 我们将搜索谓词与分类谓词 组合成新的复合谓词 最后 更新 Query 构造器 以接收这个新的复合谓词 完成这些更新后 我可以轻点搜索栏 输入一些文本来筛选行程 甚至通过分段控件进一步缩小范围 筛选和排序只是 我们优化查询与获取的几种方式 让我们探讨几种其他优化 SwiftData 数据获取的方式 以版本 1 到版本 2 的 自定义迁移阶段为例 我们将使用 willMigrate 代码块 来获取所有行程数据 但在去重逻辑中 由于版本 2 中仅需确保 name 属性的唯一性 因此我只需访问这一项属性 我用它来确保没有其他重复项 由于 name 是我唯一访问的属性 我可以更新 fetchDescriptor 使用 propertiesToFetch 来指定只获取 name 属性 这样在迁移过程中 Trip 模型就只会加载所需的数据 此外 如果我们知道 可能会遍历某个特定的关系 例如 在这个例子中 我知道如果发现重复项 我会重新分配住宿信息 我们可以通过类似的方式 利用 relationshipsToPrefetch 来优化性能 让我们在这里添加 livingAccommodation 关系
既然我们已经采用了预取属性 我们还可以更新“SampleTrips” 小组件中的现有代码 使它的性能更好 在“SampleTrips”小组件中 我们有一个查询 用于获取最近的行程 不过 我们可以优化它 使它只获取单个值 目前 小组件代码仅利用了 获取结果的第一个值 但我们可以通过设置 获取限制来提高效率
设置获取限制后 小组件只会获取 符合谓词的第一条行程 而无需担心未来 可能计划了太多行程的情况 在优化了查询和获取逻辑之后 我们再来探讨如何感知模型的变更 所有持久化模型都具备可观察性 因此我们可以利用 withObservationTracking 来响应模型中关键属性的变化 如果我们想监测行程的 开始和结束日期是否发生变动 可以添加一个 datesChangedAlert 函数 这样当用户修改日期时 系统就会触发一个提醒
通过这种方式 我们可以观察 PersistentModel 中的许多本地变更 这对于监测本地修改非常有用 如果想了解更多关于 Observable 的最新特性 可以参考“Swift 的新功能” 但是 并非所有更改都是可观察的 只有当前进程内对模型的修改 才会被捕获 而来自其他进程 如小组件或扩展程序 对数据存储的修改 甚至 App 内 另一个模型容器的变更 则不会被捕获 当你的 App 在本地或内部 发生变更时 例如多个模型上下文 共享同一个模型容器 这些模型上下文可以感知彼此的修改 对于 Query 而言 这些变更会被自动应用 然而 如果你使用的是 模型上下文的 fetch API 那么另一个模型上下文 所做的更改将不会被立即感知 除非触发重新获取
此外 外部操作 如小组件保存数据 或其他 App 写入 共享的 App Group 容器 也可能修改你的数据 这些变更会自动 更新基于 Query 的视图 但 fetch 操作仍需重新获取数据 而重新获取可能代价高昂 尤其是在模型处理所关注的属性 并未发生变化的情况下
幸运的是 SwiftData 保留了 持久化的历史记录 我们可以追踪哪些模型发生了变更 变更时间 变更来源 甚至具体更新的属性 此外 我们在 Trip 的多个属性上 启用了 preservedValueOnDeletion 因此当行程被删除时 历史记录会保留一个墓碑标记 供我们解析以识别被删除的行程 更多关于历史记录的内容 请参阅 WWDC24 的“使用 SwiftData 历史记录 API 跟踪模型更改”
我们可以利用持久化历史记录 来判断是否需要重新获取数据 首先需要从容器中 获取最新的历史记录标记 这个标记就像书签一样 能帮我们记录上次读取数据库的位置 就像在最爱的小说里 标记上次读到哪一页那样 接着 我们通过为默认历史事务 配置历史描述符 来建立历史记录获取机制 不过 如果存在大量持久化历史记录 我们可能会获取大量数据 却只用到最后一个记录作为标记 好在 iOS 26 新增了按排序 获取历史记录的功能 我们可以指定任意事务属性 如 author 或 transactionIdentifier 作为排序键 来整理历史结果 让我们采用这个新特性 按 transactionIdentifier 逆向排序 这样最新的事务会排在最前面 既然我们只需要关注最新的那条事务 那就把结果限制为 1 即可 只需完成这些步骤 我们就能高效地获取 最新的历史记录标记并存储它 让我们保存这个标记供后续使用 并在未来的历史记录获取中调用它 现在 当新的变更发生时 比如小组件更新了某条行程数据 系统就会在历史记录中新增条目 我们的 App 只需查询历史记录 就能检测自上次标记以来 是否发生了值得关注的变更 现在 我们有了存储的历史记录标记 就可以构建一个谓词 让它只获取这个标记之后的历史记录 为了仅筛选出我们关心的变更 我们需要明确指定 想要监测变更的实体 在这里 我们只关注 行程是否发生变化 或在小组件已确认住宿安排的情况下 住宿安排是否有变动 通过在变更谓词中使用这些实体名称 就能过滤出我们目标类型的 历史记录变更 最后 结合令牌谓词和变更谓词 我们构建出复合谓词 经过这些调整后 在获取历史记录时 系统只会返回令牌时间点之后的记录 并且仅涉及当前需要处理的实体 通过优化历史记录获取机制 如果没有相关变更 就能避免重复拉取数据 幸运的是 SwiftData 的历史记录 功能让这一切变得简单 至此 我们已经掌握了如何观察 模型和数据的 本地与远程变更 希望本视频能帮助你 深入理解 SwiftData 并高效实现数据持久化需求 在构建模型关系图时 请慎重考虑继承结构是否适用 并评估关系图演进时的迁移影响 获取数据时 建议构建功能更丰富 性能更优的 获取器和查询语句 及时感知数据变更的能力至关重要 观察机制和持久化历史记录 正是为此而生 这些就是全部内容 祝你探索之旅愉快!
-
-
1:07 - Import SwiftData and add @Model
// Trip Models decorated with @Model import Foundation import SwiftData @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @Model class BucketListItem { ... } @Model class LivingAccommodation { ... }
-
1:18 - Add modelContainer modifier
// SampleTrip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: Trip.self) } }
-
1:30 - Adopt @Query
// Trip App using @Query import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] var body: some View { NavigationSplitView { List(selection: $selection) { ForEach(trips) { trip in TripListItem(trip: trip) } } } } }
-
3:28 - Add subclasses to Trip
// Trip Model extended with two new subclasses @Model class Trip { var name: String var destination: String var startDate: Date var endDate: Date var bucketList: [BucketListItem] = [BucketListItem]() var livingAccommodation: LivingAccommodation? } @available(iOS 26, *) @Model class BusinessTrip: Trip { var perdiem: Double = 0.0 } @available(iOS 26, *) @Model class PersonalTrip: Trip { enum Reason: String, CaseIterable, Codable { case family case reunion case wellness } var reason: Reason }
-
4:03 - Update modelContainer modifier
// SampleTrip App using modelContainer Scene modifier import SwiftUI import SwiftData @main struct TripsApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Trip.self, BusinessTrip.self, PersonalTrip.self]) } }
-
7:06 - Add segmented control to drive a predicate to filter by Type
// Trip App add segmented control import SwiftUI import SwiftData struct ContentView: View { @Query var trips: [Trip] enum Segment: String, CaseIterable { case all = "All" case personal = "Personal" case business = "Business" } init() { let classPredicate: Predicate<Trip>? = { switch segment.wrappedValue { case .personal: return #Predicate { $0 is PersonalTrip } case .business: return #Predicate { $0 is BusinessTrip } default: return nil } } _trips = Query(filter: classPredicate, sort: \.startDate, order: .forward) } var body: some View { ... } }
-
8:26 - SampleTrips Versioned Schema 2.0
enum SampleTripsSchemaV2: VersionedSchema { static var versionIdentifier: Schema.Version { Schema.Version(2, 0, 0) } static var models: [any PersistentModel.Type] { [SampleTripsSchemaV2.Trip.self, BucketListItem.self, LivingAccommodation.self] } @Model class Trip { @Attribute(.unique) var name: String var destination: String @Attribute(originalName: "start_date") var startDate: Date @Attribute(originalName: "end_date") var endDate: Date var bucketList: [BucketListItem]? = [] var livingAccommodation: LivingAccommodation? ... } }
-
8:41 - SampleTrips Custom Migration Stage from Version 1.0 to 2.0
static let migrateV1toV2 = MigrationStage.custom( fromVersion: SampleTripsSchemaV1.self, toVersion: SampleTripsSchemaV2.self, willMigrate: { context in let fetchDesc = FetchDescriptor<SampleTripsSchemaV1.Trip>() let trips = try? context.fetch(fetchDesc) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil )
-
9:09 - SampleTrips Versioned Schema 3.0
enum SampleTripsSchemaV3: VersionedSchema { static var versionIdentifier: Schema.Version { Schema.Version(3, 0, 0) } static var models: [any PersistentModel.Type] { [SampleTripsSchemaV3.Trip.self, BucketListItem.self, LivingAccommodation.self] } @Model class Trip { #Unique<Trip>([\.name, \.startDate, \.endDate]) #Index<Trip>([\.name], [\.startDate], [\.endDate], [\.name, \.startDate, \.endDate]) @Attribute(.preserveValueOnDeletion) var name: String @Attribute(hashModifier:@"v3") var destination: String @Attribute(.preserveValueOnDeletion, originalName: "start_date") var startDate: Date @Attribute(.preserveValueOnDeletion, originalName: "end_date") var endDate: Date } }
-
9:33 - SampleTrips Custom Migration Stage from Version 2.0 to 3.0
static let migrateV2toV3 = MigrationStage.custom( fromVersion: SampleTripsSchemaV2.self, toVersion: SampleTripsSchemaV3.self, willMigrate: { context in let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV2.Trip>()) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil )
-
9:50 - SampleTrips Versioned Schema 4.0
@available(iOS 26, *) enum SampleTripsSchemaV4: VersionedSchema { static var versionIdentifier: Schema.Version { Schema.Version(4, 0, 0) } static var models: [any PersistentModel.Type] { [Trip.self, BusinessTrip.self, PersonalTrip.self, BucketListItem.self, LivingAccommodation.self] } }
-
10:03 - SampleTrips Lightweight Migration Stage from Version 3.0 to 4.0
@available(iOS 26, *) static let migrateV3toV4 = MigrationStage.lightweight( fromVersion: SampleTripsSchemaV3.self, toVersion: SampleTripsSchemaV4.self )
-
10:24 - SampleTrips Schema Migration Plan
enum SampleTripsMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { var currentSchemas: [any VersionedSchema.Type] = [SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self] if #available(iOS 26, *) { currentSchemas.append(SampleTripsSchemaV4.self) } return currentSchemas } static var stages: [MigrationStage] { var currentStages = [migrateV1toV2, migrateV2toV3] if #available(iOS 26, *) { currentStages.append(migrateV3toV4) } return currentStages } }
-
10:51 - Use Schema Migration Plan with ModelContainer
// SampleTrip App update modelContainer Scene modifier for migrated container @main struct TripsApp: App { let container: ModelContainer = { do { let schema = Schema(versionedSchema: SampleTripsSchemaV4.self) container = try ModelContainer( for: schema, migrationPlan: SampleTripsMigrationPlan.self) } catch { ... } return container }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } }
-
11:48 - Add search predicate to Query
// Trip App add search text to predicate struct ContentView: View { @Query var trips: [Trip] init( ... ) { let classPredicate: Predicate<Trip>? = { switch segment.wrappedValue { case .personal: return #Predicate { $0 is PersonalTrip } case .business: return #Predicate { $0 is BusinessTrip } default: return nil } } let searchPredicate = #Predicate<Trip> { searchText.isEmpty ? true : $0.name.localizedStandardContains(searchText) || $0.destination.localizedStandardContains(searchText) } let fullPredicate: Predicate<Trip> if let classPredicate { fullPredicate = #Predicate { classPredicate.evaluate($0) && searchPredicate.evaluate($0)} } else { fullPredicate = searchPredicate } _trips = Query(filter: fullPredicate, sort: \.startDate, order: .forward) } var body: some View { ... } }
-
12:31 - Tailor SwiftData Fetch in Custom Migration Stage
static let migrateV1toV2 = MigrationStage.custom( fromVersion: SampleTripsSchemaV1.self, toVersion: SampleTripsSchemaV2.self, willMigrate: { context in var fetchDesc = FetchDescriptor<SampleTripsSchemaV1.Trip>() fetchDesc.propertiesToFetch = [\.name] let trips = try? context.fetch(fetchDesc) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil )
-
13:11 - Add relationshipsToPrefetch in Custom Migration Stage
static let migrateV1toV2 = MigrationStage.custom( fromVersion: SampleTripsSchemaV1.self, toVersion: SampleTripsSchemaV2.self, willMigrate: { context in var fetchDesc = FetchDescriptor<SampleTripsSchemaV1.Trip>() fetchDesc.propertiesToFetch = [\.name] fetchDesc.relationshipKeyPathsForPrefetching = [\.livingAccommodation] let trips = try? context.fetch(fetchDesc) // De-duplicate Trip instances here... try? context.save() }, didMigrate: nil )
-
13:28 - Update Widget to harness fetchLimit
// Widget code to get new Timeline Entry func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) { let currentDate = Date.now var fetchDesc = FetchDescriptor(sortBy: [SortDescriptor(\Trip.startDate, order: .forward)]) fetchDesc.predicate = #Predicate { $0.endDate >= currentDate } fetchDesc.fetchLimit = 1 let modelContext = ModelContext(DataModel.shared.modelContainer) if let upcomingTrips = try? modelContext.fetch(fetchDesc) { if let trip = upcomingTrips.first { ... } } }
-
16:24 - Fetch the last transaction efficiently
// Fetch history with sortBy and fetchlimit to get the last token var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>() historyDesc.sortBy = [.init(\.transactionIdentifier, order: .reverse)] historyDesc.fetchLimit = 1 let transactions = try context.fetchHistory(historyDesc) if let transaction = transactions.last { historyToken = transaction.token }
-
17:29 - Fetch History after the given token and only for the entities of concern
// Changes AFTER the last known token let tokenPredicate = #Predicate<DefaultHistoryTransaction> { $0.token > historyToken } // Changes for ONLY entities of concern let entityNames = [LivingAccommodation.self, Trip.self] let changesPredicate = #Predicate<DefaultHistoryTransaction> { $0.changes.contains { change in entityNames.contains(change.changedPersistentIdentifier.entityName) } } let fullPredicate = #Predicate<DefaultHistoryTransaction> { tokenPredicate.evaluate($0) && changesPredicate.evaluate($0) } let historyDesc = HistoryDescriptor<DefaultHistoryTransaction>(predicate: fullPredicate) let transactions = try context.fetchHistory(historyDesc)
-
-
- 0:00 - 简介
SwiftData 支持跨所有 Apple 平台对 App 数据进行建模并持久保留数据。此框架简化了数据持久保留、架构建模和迁移、图形管理和 CloudKit 同步。类继承是 iOS 26 中新引入的一项功能,支持通过继承来生成模型图形。
- 2:11 - 利用类继承
类继承是一种强大的工具,尤其适用于模型之间存在自然层级关系并共享共同特性的场景。继承机制支持创建子类,从父类继承属性和行为,从而促进代码复用,并保持结构化的组织方式。 “SampleTrips”App 应用了继承机制对不同类型的行程进行建模,例如个人行程和商务行程。每个子类都继承了 Trip 模型的基本属性,并添加了与其子域相关的特定属性。这种方法能够实现对数据更具针对性且更高效的表示。 应谨慎使用继承机制。当模型建立“is-a”的关系,并且查询同时涉及父类及其子类时,继承是合适的选择。如果模型只是共享一些通用属性,但不存在自然的层级关系,那么协议遵从性会是更合适的选择。选择继承,还是协议遵从性,还取决于对数据执行搜索的深度。
- 7:39 - 通过迁移优化数据
“SampleTrips”App 在不同 iOS 版本之间的数据迁移流程,是在系统升级过程中确保用户数据得以保留的一个典型示例。该 App 的架构在多个版本中不断演进: iOS 17 引入了 SwiftData 和架构 2.0 版本,使行程名称具有唯一性并重命名了属性。 iOS 18 添加了 3.0 版,运用了 Index 和 Unique 宏,并在删除时保留了属性。使用了自定“MigrationStages”执行数据去重。 iOS 26 推出了 4.0 版,其中包含了子类。从版本 3.0 升级到 4.0 时,需要一个轻量级的“MigrationStage”。 通过按正确顺序封装“VersionedSchemas”和“MigrationStages”,可构建一个“SchemaMigrationPlan”。然后,在为“SampleTrips”创建“ModelContainer”时,会应用“SchemaMigrationPlan”,从而在保留用户数据的同时,实现对所有先前迭代版本的无缝迁移。
- 11:27 - 调整获取的数据
为了探索如何优化查询和获取,“SampleTrips”App 重新引入了搜索栏功能。该 App 会根据客户端的搜索构建一个谓词,然后将其与类谓词组合,用于筛选行程。 除了搜索之外,这些技术还增强了获取性能: 在迁移过程中,仅使用“propertiesToFetch”来获取必要的属性。 “relationshipsToPrefetch”可用于优化关系遍历。 在小组件代码中设置“fetchLimit”,以仅检索最近的一次行程,从而提高效率。
- 13:54 - 观测数据更改
SwiftData 的 Observable 功能可帮助你响应对“PersistentModels”所做的本地更改。但是,并非所有变化都是可观察到的。来自其他进程、外部操作或 App 中不同模型上下文的更改,通常需要重新获取数据,而这可能需要付出高昂的成本。 要优化重新获取,你可以使用 SwiftData 的持久历史记录功能。通过获取最新的历史记录令牌并将其用作标记,你可以构建谓词,仅获取在该令牌之后发生的历史记录,并针对感兴趣的特定实体进行筛选。这种方法使 App 能够确定是否需要重新获取数据,从而避免不必要的数据检索并提高性能。
- 18:28 - 后续步骤
构建模型关系图时,请谨慎考虑继承和迁移的影响。优化数据获取器和查询以提升性能。利用观察和持久化历史记录来跟踪数据变更。