swiftdata-inheritance
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftData Class Inheritance
SwiftData类继承
Guide for implementing class inheritance in SwiftData models. Covers when to use inheritance versus enums or protocols, how to annotate subclasses, query across hierarchies, and avoid common pitfalls with schema migrations and relationship modeling.
SwiftData模型中实现类继承的指南。涵盖何时使用继承而非枚举或协议、如何标注子类、跨层级查询,以及如何避免模式迁移和关系建模中的常见陷阱。
When This Skill Activates
适用场景
- User is designing a SwiftData model hierarchy with shared base properties
- User asks about on subclasses or how inheritance works in SwiftData
@Model - User needs to query across a type hierarchy (all trips vs only business trips)
- User is deciding between inheritance, enums, or protocols for model variants
- User has issues with polymorphic relationships or type casting in SwiftData
- User is migrating a Core Data inheritance hierarchy to SwiftData
- 用户正在设计带有共享基础属性的SwiftData模型层级
- 用户询问子类上的用法或SwiftData中继承的工作机制
@Model - 用户需要跨类型层级查询(所有行程 vs 仅商务行程)
- 用户正在为模型变体选择继承、枚举或协议
- 用户在SwiftData中遇到多态关系或类型转换问题
- 用户正在将Core Data继承层级迁移到SwiftData
Decision Tree
决策树
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的一种)
| |
| +-- 子类是否添加了大量独特属性或行为?
| | +-- 是 --> 使用类继承(本指南内容)
| | +-- 否,仅1-2个区分字段 --> 在基础模型上使用枚举属性
| |
| +-- 是否需要查询“所有行程”和“仅商务行程”?
| +-- 是 --> 继承可同时满足这两种需求
| +-- 仅需单次查询一种类型 --> 枚举过滤更简单
|
+-- 否:模型仅共享少量属性
| +-- 使用协议一致性(无需SwiftData继承)
|
+-- 不确定:两种方式均可
+-- 优先选择基础模型上的枚举(模式更简单,迁移更易)
+-- 若变体差异显著,后续可升级为继承When to Use Inheritance
何时使用继承
- There is a meaningful IS-A relationship (a fundamentally IS a
BusinessTrip)Trip - Subclasses add substantial unique stored properties
- You need deep queries (fetch all instances regardless of subtype) and shallow queries (fetch only
Trip)BusinessTrip - Polymorphic relationships are required (a array holding mixed subtypes)
[Trip]
- 存在有意义的IS-A关系(本质上是一种
BusinessTrip)Trip - 子类添加了大量独特的存储属性
- 需要深度查询(获取所有实例,无论子类型)和浅层查询(仅获取
Trip)BusinessTrip - 需要多态关系(数组可容纳混合子类型)
[Trip]
When to Avoid Inheritance
何时避免继承
- Subclasses share only a few properties -- use a protocol instead
- A boolean flag or enum could represent the distinction without separate classes
- You want to minimize schema migration complexity
- The hierarchy would go deeper than two levels
- 子类仅共享少量属性——改用协议
- 布尔标记或枚举即可区分不同类型,无需单独类
- 希望最小化模式迁移复杂度
- 层级超过两层
API Patterns
API模式
Base Model Declaration
基础模型声明
Apply to the base class. All persistent properties live here.
@Modelswift
@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
}
}将应用于基类。所有持久化属性都定义在此处。
@Modelswift
@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
}
}Subclass Declaration
子类声明
Apply to each subclass. Call and add subclass-specific stored properties.
@Modelsuper.init()swift
@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)
}
}将应用于每个子类。调用并添加子类专属的存储属性。
@Modelsuper.init()swift
@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)
}
}Relationships Across the Hierarchy
跨层级关系
Relationships defined on the base class apply to all subclasses. The inverse can point to a base class property and will resolve to the correct subclass at runtime.
swift
@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 }
}基类上定义的关系适用于所有子类。反向关系可指向基类属性,运行时会自动解析为正确的子类。
swift
@Model
class Accommodation {
var name: String
// 指向Trip——运行时可能是BusinessTrip或PersonalTrip
@Relationship(inverse: \Trip.accommodations)
var trip: Trip?
init(name: String) { self.name = name }
}ModelContainer Configuration
ModelContainer配置
Register the base class. SwiftData discovers subclasses automatically.
swift
// Register Trip -- BusinessTrip and PersonalTrip are included automatically
let container = try ModelContainer(for: Trip.self, Accommodation.self, Itinerary.self)注册基类。SwiftData会自动发现子类。
swift
// 注册Trip——BusinessTrip和PersonalTrip会自动包含在内
let container = try ModelContainer(for: Trip.self, Accommodation.self, Itinerary.self)Querying Hierarchies
层级查询
Deep Query (All Subclasses)
深度查询(所有子类)
Querying the base class returns instances of every subclass.
swift
// Returns Trip, BusinessTrip, and PersonalTrip instances
@Query(sort: \Trip.startDate)
var allTrips: [Trip]查询基类会返回所有子类的实例。
swift
// 返回Trip、BusinessTrip和PersonalTrip实例
@Query(sort: \Trip.startDate)
var allTrips: [Trip]Type Filtering with Predicate
使用谓词进行类型过滤
Narrow results to a specific subclass using or in a .
isas?#Predicateswift
// 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]在中使用或将结果限定为特定子类。
#Predicateisas?swift
// 仅返回BusinessTrip实例
let businessOnly = #Predicate<Trip> { trip in
trip is BusinessTrip
}
@Query(filter: #Predicate<Trip> { $0 is BusinessTrip }, sort: \Trip.startDate)
var businessTrips: [Trip]Subclass Property Filtering
子类属性过滤
Access subclass-specific properties with conditional casting inside the predicate.
swift
let vacationTrips = #Predicate<Trip> { trip in
if let personal = trip as? PersonalTrip {
personal.reason == .vacation
} else {
false
}
}在谓词内使用条件转换访问子类专属属性。
swift
let vacationTrips = #Predicate<Trip> { trip in
if let personal = trip as? PersonalTrip {
personal.reason == .vacation
} else {
false
}
}Enum-Based Filter Switching in UI
UI中基于枚举的过滤切换
A common pattern for filter controls that switch between all trips and a specific type.
swift
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
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("筛选", selection: $filter) {
ForEach(TripFilter.allCases) { f in
Text(f.rawValue.capitalized).tag(f)
}
}
.pickerStyle(.segmented)
ForEach(filteredTrips) { trip in
TripRowView(trip: trip)
}
}
}
}Type Casting at Runtime
运行时类型转换
Use standard Swift casting to access subclass-specific properties in views.
swift
if let business = trip as? BusinessTrip {
LabeledContent("Company", value: business.company)
}
if let personal = trip as? PersonalTrip {
LabeledContent("Reason", value: personal.reason.rawValue)
}在视图中使用标准Swift转换安全访问子类专属属性。
swift
if let business = trip as? BusinessTrip {
LabeledContent("公司", value: business.company)
}
if let personal = trip as? PersonalTrip {
LabeledContent("原因", value: personal.reason.rawValue)
}Top Mistakes
常见错误
1. Missing @Model on Subclass
1. 子类缺少@Model
The macro must appear on both the base class and every subclass. Omitting it on a subclass causes its unique properties to be silently ignored.
@Modelswift
// WRONG -- subclass properties not persisted
class BusinessTrip: Trip {
var company: String // not saved
...
}
// RIGHT
@Model
class BusinessTrip: Trip {
var company: String // persisted correctly
...
}基类和每个子类都必须添加宏。子类省略该宏会导致其专属属性被静默忽略。
@Modelswift
// 错误——子类属性不会被持久化
class BusinessTrip: Trip {
var company: String // 不会保存
...
}
// 正确
@Model
class BusinessTrip: Trip {
var company: String // 正确持久化
...
}2. Deep Hierarchies
2. 过深的层级
Keep to one level of subclassing. Going beyond two levels (base + one tier) increases schema complexity and migration risk.
swift
// 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 { ... }仅保留一层子类。层级超过两层(基类+一层子类)会增加模式复杂度和迁移风险。
swift
// 错误——三层深度
@Model class InternationalBusinessTrip: BusinessTrip { ... } // 避免使用
// 正确——扁平化:基类+一层子类
@Model class Trip { ... }
@Model class BusinessTrip: Trip { ... }
@Model class PersonalTrip: Trip { ... }3. Using Inheritance When an Enum Would Suffice
3. 可用枚举却使用继承
If the only difference is a type tag and one or two optional fields, an enum on the base model is simpler.
swift
// 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?
}如果仅差异在于类型标签和一两个可选字段,在基础模型上使用枚举更简单。
swift
// 错误——仅为分类标签使用继承
@Model class DomesticTrip: Trip { }
@Model class InternationalTrip: Trip { var passportRequired: Bool = true }
// 正确——在基础模型上使用枚举属性
@Model class Trip {
enum Category: String, Codable { case domestic, international }
var name: String
var category: Category
var passportRequired: Bool?
}4. Forgetting super.init()
4. 忘记调用super.init()
Subclass initializers must call with all required base properties. Missing this causes incomplete or corrupt records.
super.init()swift
// 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)
}子类初始化器必须调用并传入所有必填的基类属性。遗漏会导致记录不完整或损坏。
super.init()swift
// 错误——基类属性未初始化
init(company: String) {
self.company = company
// 缺少super.init(name:startDate:endDate:)
}
// 正确——始终调用super.init()
init(name: String, startDate: Date, endDate: Date, company: String) {
self.company = company
super.init(name: name, startDate: startDate, endDate: endDate)
}5. Registering Subclasses Separately in ModelContainer
5. 在ModelContainer中单独注册子类
SwiftData discovers subclasses automatically. Register only the base class.
swift
// UNNECESSARY
let container = try ModelContainer(for: Trip.self, BusinessTrip.self, PersonalTrip.self)
// RIGHT
let container = try ModelContainer(for: Trip.self)SwiftData会自动发现子类。仅注册基类即可。
swift
// 不必要
let container = try ModelContainer(for: Trip.self, BusinessTrip.self, PersonalTrip.self)
// 正确
let container = try ModelContainer(for: Trip.self)Review Checklist
审核清单
When reviewing code that uses SwiftData class inheritance, verify each item:
- is applied to the base class AND every subclass
@Model - Each subclass calls with all required base properties
super.init() - Hierarchy is shallow (base + one level of subclasses, no deeper)
- The IS-A relationship is meaningful -- not just a type tag that could be an enum
- is used on fields needed after deletion (sync IDs, audit trails)
@Attribute(.preserveValueOnDeletion) - Relationships use parameters correctly, pointing to the base class property
inverse: - is specified on owning side (
@Relationship(deleteRule:),.cascade, or.nullify).deny - Deep queries (base class fetch) and shallow queries (type-filtered) both work as expected
- Type casting () is used safely with
as? Subclassin views and logicif let - ModelContainer registers the base class (subclasses are auto-discovered)
- Schema migration plan exists if the hierarchy will evolve (adding/removing subclasses)
- Enum-based filter pattern is used for UI that switches between type views
审核使用SwiftData类继承的代码时,需验证以下各项:
- 基类和每个子类都添加了
@Model - 每个子类都调用了并传入所有必填基类属性
super.init() - 层级较浅(基类+一层子类,无更深层级)
- IS-A关系有实际意义——并非仅可用枚举替代的类型标签
- 对删除后仍需保留的字段(同步ID、审计追踪)使用了
@Attribute(.preserveValueOnDeletion) - 关系正确使用参数,指向基类属性
inverse: - 所属方指定了(
@Relationship(deleteRule:)、.cascade或.nullify).deny - 深度查询(基类获取)和浅层查询(类型过滤)均按预期工作
- 在视图和逻辑中安全使用结合类型转换(
if let)as? Subclass - ModelContainer仅注册基类(子类自动发现)
- 若层级会演进(添加/移除子类),存在模式迁移计划
- UI中切换类型视图时使用了基于枚举的过滤模式
Cross-Reference
交叉引用
- For SwiftData repository and architecture patterns, see
macos/swiftdata-architecture/ - For SwiftData concurrency with @ModelActor, see
swift/concurrency-patterns/ - For persistence setup generator, see
generators/persistence-setup/
- 关于SwiftData仓库和架构模式,请查看
macos/swiftdata-architecture/ - 关于使用@ModelActor的SwiftData并发,请查看
swift/concurrency-patterns/ - 关于持久化设置生成器,请查看
generators/persistence-setup/
References
参考资料
- Preserving your app's model data across launches
- SwiftData documentation
- Apple doc:
/Users/ravishankar/Downloads/docs/SwiftData-Class-Inheritance.md
- Preserving your app's model data across launches
- SwiftData documentation
- Apple文档:
/Users/ravishankar/Downloads/docs/SwiftData-Class-Inheritance.md