swiftdata-inheritance について
SwiftDataのモデルを設計する開発者向けのコーディングスキルです。継承の適用場面、クラス階層の定義とクエリ、永続化やマイグレーションの注意点を解説します。
主な機能
- 01継承・列挙型・プロトコルの比較
- 02基底モデルとサブクラスの宣言
- 03モデル階層をまたぐクエリ
- 04ポリモーフィックな関係とキャスト
- 05マイグレーションとスキーマの指針
活用シーン
- 01モデルの種類を表す方法の選定
- 02全インスタンスまたは特定サブクラスの検索
- 03SwiftDataの継承コードのレビュー
インストール
コマンドをコピーしてターミナルで実行します。
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]配列など)
継承を避ける場合
- サブクラス間で共有するプロパティが少数のみの場合は、代わりにプロトコルを使います
- 別々のクラスを作らなくても、ブール値フラグや列挙型で違いを表現できる
- スキーマ移行の複雑さを最小限に抑えたい
- 階層が2段階を超える
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]
サブクラスのプロパティによる絞り込み
predicate 内で条件付きキャストを使って、サブクラス固有のプロパティにアクセスします。
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. 深い階層
継承階層は1段階(ベースクラス+1階層)にとどめます。
// 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. 列挙型で十分な場合に継承を使う
違いが型タグと1つか2つのオプショナルフィールドだけであれば、ベースモデルに列挙型を使うほうがシンプルです。
// 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()を呼び出している - 継承階層が浅い(基底クラス+サブクラスは1階層のみで、それより深くない)
- 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— 存在する場合は読み込み、存在しない場合は何もせずスキップしてください。