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. 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/ を参照してください

参考資料

コメント