关于 swiftdata-inheritance
面向 SwiftData 开发者的编码技能,介绍类继承的适用场景、层级模型的声明与查询,以及如何避免持久化和迁移问题。
主要功能
- 01比较继承、枚举与协议
- 02声明基类和子类模型
- 03查询整个模型层级
- 04处理多态关系与类型转换
- 05提供迁移和架构指导
适用场景
- 01为模型变体选择表示方式
- 02查询全部实例或指定子类
- 03检查 SwiftData 继承代码
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ rshankras/ claude-code-apple-skills --skill swiftdata-inheritance安装后在 Claude Code 中输入/swiftdata-inheritance确认已加载。
详细介绍
SwiftData 类继承
SwiftData 模型中实现类继承的指南。介绍何时使用继承而不是枚举或协议、如何为子类添加注解、如何跨层级查询,以及如何避免架构迁移和关系建模中的常见问题。
此技能何时启用
- 用户正在设计具有共享基础属性的 SwiftData 模型层级
- 用户询问如何在子类上使用
@Model,或 SwiftData 中的继承机制 - 用户需要跨类型层级查询(所有行程,或仅商务行程)
- 用户正在决定使用继承、枚举还是协议来表示模型变体
- 用户遇到 SwiftData 中多态关系或类型转换问题
- 用户正在将 Core Data 继承层级迁移到 SwiftData
决策树
Do your model variants share a common identity and most properties?
|
+-- YES: Clear IS-A relationship (BusinessTrip IS-A Trip)
| |
| +-- Subclasses add significant unique properties or behavior?
| | +-- YES --> Use class inheritance (this skill)
| | +-- NO, just 1-2 distinguishing fields --> Use enum property on base model
| |
| +-- Need to query "all trips" AND "only business trips"?
| +-- YES --> Inheritance gives you both for free
| +-- Only one type at a time --> Enum filter is simpler
|
+-- NO: Models share only a few properties
| +-- Use protocol conformance (no SwiftData inheritance needed)
|
+-- UNCERTAIN: Could go either way
+-- Prefer enum on base model (simpler schema, easier migrations)
+-- Promote to inheritance later if variants diverge significantly
何时使用继承
- 存在有意义的 IS-A 关系(
BusinessTrip本质上是Trip) - 子类新增了大量独有的存储属性
- 需要深层查询(获取所有
Trip实例,无论其子类型)和浅层查询(仅获取BusinessTrip) - 需要多态关系(
[Trip]数组中包含多种子类型)
何时避免继承
- 子类只共享少数属性——改用协议
- 使用布尔标志或枚举即可表示差异,无需单独的类
- 希望尽量降低架构迁移的复杂性
- 层级深度超过两层
API 模式
基类声明
为基类应用 @Model。所有持久化属性都定义在这里。
@Model
class Trip {
var name: String
var startDate: Date
var endDate: Date
@Attribute(.preserveValueOnDeletion)
var identifier: UUID
@Relationship(deleteRule: .cascade, inverse: \Accommodation.trip)
var accommodations: [Accommodation] = []
init(name: String, startDate: Date, endDate: Date) {
self.identifier = UUID()
self.name = name
self.startDate = startDate
self.endDate = endDate
}
}
子类声明
为每个子类应用 @Model。调用 super.init(),并添加子类专属的存储属性。
@Model
class BusinessTrip: Trip {
var company: String
var expenseReport: String?
var meetingAgenda: String?
init(name: String, startDate: Date, endDate: Date, company: String) {
self.company = company
super.init(name: name, startDate: startDate, endDate: endDate)
}
}
@Model
class PersonalTrip: Trip {
enum Reason: String, Codable {
case vacation
case family
case adventure
}
var reason: Reason
var companions: [String] = []
init(name: String, startDate: Date, endDate: Date, reason: Reason) {
self.reason = reason
super.init(name: name, startDate: startDate, endDate: endDate)
}
}
跨层级关系
在基类上定义的关系适用于所有子类:
@Model
class Accommodation {
var name: String
// Points to Trip -- could be BusinessTrip or PersonalTrip at runtime
@Relationship(inverse: \Trip.accommodations)
var trip: Trip?
init(name: String) { self.name = name }
}
ModelContainer 配置
注册基类。SwiftData 会自动发现子类。
// Register Trip -- BusinessTrip and PersonalTrip are included automatically
let container = try ModelContainer(for: Trip.self, Accommodation.self, Itinerary.self)
层级查询
深层查询(所有子类)
查询基类会返回每个子类的实例。
// Returns Trip, BusinessTrip, and PersonalTrip instances
@Query(sort: \Trip.startDate)
var allTrips: [Trip]
使用 Predicate 筛选类型
在 #Predicate 中使用 is 或 as?,将结果缩小到特定子类。
// Only BusinessTrip instances
let businessOnly = #Predicate<Trip> { trip in
trip is BusinessTrip
}
@Query(filter: #Predicate<Trip> { $0 is BusinessTrip }, sort: \Trip.startDate)
var businessTrips: [Trip]
按子类属性筛选
在谓词中使用条件类型转换,访问子类专属的属性。
let vacationTrips = #Predicate<Trip> { trip in
if let personal = trip as? PersonalTrip {
personal.reason == .vacation
} else {
false
}
}
在 UI 中使用基于枚举的筛选切换
用于筛选控件的一种常见模式,可在所有行程和特定类型之间切换。
enum TripFilter: String, CaseIterable, Identifiable {
case all, business, personal
var id: String { rawValue }
}
struct TripListView: View {
@State private var filter: TripFilter = .all
@Query(sort: \Trip.startDate) var allTrips: [Trip]
var filteredTrips: [Trip] {
switch filter {
case .all: return allTrips
case .business: return allTrips.filter { $0 is BusinessTrip }
case .personal: return allTrips.filter { $0 is PersonalTrip }
}
}
var body: some View {
List {
Picker("Filter", selection: $filter) {
ForEach(TripFilter.allCases) { f in
Text(f.rawValue.capitalized).tag(f)
}
}
.pickerStyle(.segmented)
ForEach(filteredTrips) { trip in
TripRowView(trip: trip)
}
}
}
}
运行时类型转换
使用标准 Swift 类型转换,在视图中访问子类专属的属性。
if let business = trip as? BusinessTrip {
LabeledContent("Company", value: business.company)
}
if let personal = trip as? PersonalTrip {
LabeledContent("Reason", value: personal.reason.rawValue)
}
常见错误
1. 子类缺少 @Model
基类和每个子类都必须使用 @Model 宏。
// WRONG -- subclass properties not persisted
class BusinessTrip: Trip {
var company: String // not saved
...
}
// RIGHT
@Model
class BusinessTrip: Trip {
var company: String // persisted correctly
...
}
2. 层级过深
将层级限制为一层子类(基类 + 一层子类)。
// WRONG -- three levels deep
@Model class InternationalBusinessTrip: BusinessTrip { ... } // avoid
// RIGHT -- flat: base + one level
@Model class Trip { ... }
@Model class BusinessTrip: Trip { ... }
@Model class PersonalTrip: Trip { ... }
3. 在枚举足以满足需求时使用继承
如果唯一差异是类型标记和一两个可选字段,在基类上使用枚举会更简单。
// WRONG -- inheritance just for a category label
@Model class DomesticTrip: Trip { }
@Model class InternationalTrip: Trip { var passportRequired: Bool = true }
// RIGHT -- enum property on the base model
@Model class Trip {
enum Category: String, Codable { case domestic, international }
var name: String
var category: Category
var passportRequired: Bool?
}
4. 忘记 super.init()
子类初始化器必须使用所有必需的基类属性调用 super.init()。遗漏会导致记录不完整或损坏。
// WRONG -- base properties uninitialized
init(company: String) {
self.company = company
// Missing super.init(name:startDate:endDate:)
}
// RIGHT -- always call super.init()
init(name: String, startDate: Date, endDate: Date, company: String) {
self.company = company
super.init(name: name, startDate: startDate, endDate: endDate)
}
5. 在 ModelContainer 中单独注册子类
只注册基类——参见上文的“ModelContainer 配置”。
// UNNECESSARY
let container = try ModelContainer(for: Trip.self, BusinessTrip.self, PersonalTrip.self)
// RIGHT
let container = try ModelContainer(for: Trip.self)
审查清单
审查使用 SwiftData 类继承的代码时,请核对以下各项:
- 基类和每个子类都应用了
@Model - 每个子类都调用了
super.init(),并传入所有必需的基类属性 - 继承层次较浅(基类 + 一层子类,不再深入)
- IS-A 关系有实际意义——不只是一个本可以用枚举表示的类型标签
- 对删除后仍需使用的字段(同步 ID、审计跟踪)使用了
@Attribute(.preserveValueOnDeletion) - 关系正确使用
inverse:参数,并指向基类属性 - 在关系的所有者一侧指定了
@Relationship(deleteRule:)(.cascade、.nullify或.deny) - 深层查询(获取基类)和浅层查询(按类型筛选)都能按预期工作
- 在视图和逻辑中使用
if let安全地进行类型转换(as? Subclass) - ModelContainer 注册了基类(子类会自动发现)
- 如果继承层次会发生变化(添加或移除子类),则需制定架构迁移计划
- 对于需要在不同类型视图间切换的 UI,使用基于枚举的筛选模式
交叉参考
- 有关 SwiftData 仓库和架构模式,请参阅
macos/swiftdata-architecture/ - 有关 使用 @ModelActor 的 SwiftData 并发,请参阅
swift/concurrency-patterns/ - 有关 持久化设置生成器,请参阅
generators/persistence-setup/
参考资料
- 在不同启动会话间保留应用的模型数据
- SwiftData 文档
- 本地捕获的文档(可选):
~/Downloads/docs/SwiftData-Class-Inheritance.md— 如果存在则阅读;如果不存在则静默跳过。
评论
支持 Markdown