SwiftData: Dive into inheritance and schema migration
App Services 进阶 2m

SwiftData: Dive into inheritance and schema migration

2025年6月9日

在 Apple 官方观看视频

一句话判断

SwiftData 终于支持 class inheritance 了。但别高兴太早——继承只在你的模型确实构成 “is-a” 关系、且查询模式同时需要 deep 和 shallow search 时才值得用。Apple 同时给出了从 iOS 17 到 iOS 26 的完整迁移方案,很实用。

这场 Session 讲了什么

Rishi Verma 延续了 SampleTrips app 的故事线,从 iOS 17 一路演进到 iOS 26。

Class inheritance 支持: Trip 模型作为基类,派生出 PersonalTrip(增加 reason 枚举)和 BusinessTrip(增加 perdiem 属性)。子类需要 @available(iOS 26, *) 标记。schema 注册时把子类加到 modelContainer modifier 的列表中。

什么时候用继承: Apple 给了明确的判断标准——

  1. 模型形成自然层级关系(is-a 关系)。
  2. 你同时需要 deep search(查所有 Trip)和 shallow search(只查 PersonalTrip)。
  3. 如果你总是只查基类、从不查子类类型 → 把子类做成属性更合理。
  4. 如果你总是只查子类、从不用基类类型 → 考虑扁平化模型。

Predicate 中用 is 关键字过滤子类:

let predicate = #Predicate<Trip> { trip in
    trip is PersonalTrip
}

Schema Migration Plan: 展示了从 iOS 17(version 2)到 iOS 18(version 3)再到 iOS 26(version 4)的完整迁移链。继承相关的迁移是 lightweight migration(自动处理),之前的版本有自定义 migration stage 做去重。

查询优化:

  • propertiesToFetch 只加载需要的属性(迁移去重时只需要 name)。
  • relationshipsToPrefetch 预加载关系(迁移时知道要 reassign livingAccommodation)。
  • fetchLimit 限制结果数量(widget 只需要最近一条 trip)。

变更观察: 本地变化用 withObservationTracking(Observable protocol)。跨进程/跨 container 的变化用 SwiftData History。新增 sortBylimit 到 history fetch,可以高效获取最新 history token,避免每次全量扫描。

值得深挖的点

继承的陷阱:不要用继承来共享公共属性。 如果所有有 name 属性的模型都继承自一个基类,层级关系会变得混乱。这种场景应该用 protocol。

Deep vs shallow search 的判断很实际。 很多时候你的模型设计应该跟着查询模式走,而不是反过来。如果你发现某个子类从未被单独查询过,那它可能不应该是一个子类。

History fetch 的 sortBy + limit 是个性能利器。 以前要拿最新 token 需要 fetch 全部 history 再取最后一条。现在 sort by transactionIdentifier descending + limit 1,一步到位。

preservedValueOnDeletion 的联动。 在 iOS 18 版本中标记的 preservedValueOnDeletion 属性,在 iOS 26 用 history 做变更追踪时会提供 tombstone 信息——可以知道被删除的是哪条数据。

代码片段

完整的 Schema Migration Plan(iOS 17 -> 18 -> 26):

struct SampleTripsMigrationPlan: SchemaMigrationPlan {
    static var schemas: [VersionedSchema.Type] = [
        SampleTripsSchemaV1.self,  // iOS 17
        SampleTripsSchemaV2.self,  // iOS 17 - dedup
        SampleTripsSchemaV3.self,  // iOS 18 - unique/index
        SampleTripsSchemaV4.self,  // iOS 26 - inheritance
    ]

    static var migrateStages: [MigrationStage] = [
        MigrationStage.custom(
            fromVersion: SampleTripsSchemaV1.self,
            toVersion: SampleTripsSchemaV2.self,
            willMigrate: { context in
                // 去重逻辑
                let trips = try context.fetch(FetchDescriptor<Trip>())
                // ... dedup
            }
        ),
        MigrationStage.custom(
            fromVersion: SampleTripsSchemaV2.self,
            toVersion: SampleTripsSchemaV3.self,
            willMigrate: { context in /* dedup again */ }
        ),
        MigrationStage.lightweight(
            fromVersion: SampleTripsSchemaV3.self,
            toVersion: SampleTripsSchemaV4.self
        ),
    ]
}

@available(iOS 26, *)
struct SampleTripsSchemaV4: VersionedSchema {
    static var versionIdentifier = Schema.Version(4, 0, 0)
    static var models: [any PersistentModel.Type] = [
        Trip.self, BusinessTrip.self, PersonalTrip.self
    ]
}

高效 History fetch 避免不必要的 refetch:

// 获取最新 token
var descriptor = FetchDescriptor<DefaultHistoryTransaction>()
descriptor.sortBy = [SortDescriptor(\.transactionIdentifier, order: .reverse)]
descriptor.fetchLimit = 1
let latestToken = try modelContext.fetchHistory(descriptor).first

// 只查感兴趣的变化
let predicate = #Predicate<DefaultHistoryTransaction> { txn in
    txn.timestamp > lastTokenDate
}

最佳实践

先画查询模式再决定模型结构。 列出你的所有 fetch/query 场景,统计 deep vs shallow search 的比例。继承不是默认选择。

迁移测试要覆盖完整升级路径。 用户可能从任何历史版本直接升级到最新版。用 versioned schema + migration plan 确保每一步都能正确执行。

History token 要持久化。 存在 UserDefaults 或文件中,下次启动时从上次 token 继续,避免重复处理。

@available 标记要同步。 子类、version schema、migration stage 都需要标注 iOS 26 availability,否则编译器会报错。

还有什么值得关注

  • SwiftData 的 Query macro 会自动反映其他 model context 的变更(包括 widget 写入),但手动 fetch 不会。如果你用 fetch API,需要配合 history 判断是否需要 refetch。
  • CloudKit 同步与继承模型完全兼容,无需额外配置。
  • 配套视频:“What’s new in Swift” 有 Observable 和 withObservationTracking 的最新变化。
应用服务 Swift