mcpField
S
swiftdata-inheritance

swiftdata-inheritance

github.com/rshankras开发83878 次安装14 周前更新
代码仓库

关于 swiftdata-inheritance

面向 SwiftData 开发者的编码技能,介绍类继承的适用场景、层级模型的声明与查询,以及如何避免持久化和迁移问题。

主要功能

  1. 01比较继承、枚举与协议
  2. 02声明基类和子类模型
  3. 03查询整个模型层级
  4. 04处理多态关系与类型转换
  5. 05提供迁移和架构指导

适用场景

  1. 01为模型变体选择表示方式
  2. 02查询全部实例或指定子类
  3. 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/

参考资料

评论

支持 Markdown