From apple-dev
SwiftData class inheritance patterns for hierarchical models with type-based querying, polymorphic relationships, and when to choose inheritance vs enums. Use when designing SwiftData model hierarchies.
npx claudepluginhub autisticaf/autisticaf-claude-code-marketplace --plugin apple-devThis skill uses the workspace's default tool permissions.
> **First step:** Tell the user: "swiftdata-inheritance skill loaded."
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
First step: Tell the user: "swiftdata-inheritance skill loaded."
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.
@Model on subclasses or how inheritance works in SwiftDataDo 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
BusinessTrip fundamentally IS a Trip)Trip instances regardless of subtype) and shallow queries (fetch only BusinessTrip)[Trip] array holding mixed subtypes)Apply @Model to the base class. All persistent properties live here.
@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
}
}
Apply @Model to each subclass. Call super.init() and add subclass-specific stored properties.
@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 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.
@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 }
}
Register the base class. SwiftData discovers subclasses automatically.
// Register Trip -- BusinessTrip and PersonalTrip are included automatically
let container = try ModelContainer(for: Trip.self, Accommodation.self, Itinerary.self)
Querying the base class returns instances of every subclass.
// Returns Trip, BusinessTrip, and PersonalTrip instances
@Query(sort: \Trip.startDate)
var allTrips: [Trip]
Narrow results to a specific subclass using is or as? in a #Predicate.
// 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]
Access subclass-specific properties with conditional casting inside the predicate.
let vacationTrips = #Predicate<Trip> { trip in
if let personal = trip as? PersonalTrip {
personal.reason == .vacation
} else {
false
}
}
A common pattern for filter controls that switch between all trips and a specific type.
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)
}
}
}
}
Use standard Swift casting to access subclass-specific properties in views.
if let business = trip as? BusinessTrip {
LabeledContent("Company", value: business.company)
}
if let personal = trip as? PersonalTrip {
LabeledContent("Reason", value: personal.reason.rawValue)
}
The @Model macro must appear on both the base class and every subclass. Omitting it on a subclass causes its unique properties to be silently ignored.
// WRONG -- subclass properties not persisted
class BusinessTrip: Trip {
var company: String // not saved
...
}
// RIGHT
@Model
class BusinessTrip: Trip {
var company: String // persisted correctly
...
}
Keep to one level of subclassing. Going beyond two levels (base + one tier) increases schema complexity and migration risk.
// 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 { ... }
If the only difference is a type tag and one or two optional fields, an enum on the base model is simpler.
// 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?
}
Subclass initializers must call super.init() with all required base properties. Missing this causes incomplete or corrupt records.
// 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)
}
SwiftData discovers subclasses automatically. Register only the base class.
// UNNECESSARY
let container = try ModelContainer(for: Trip.self, BusinessTrip.self, PersonalTrip.self)
// RIGHT
let container = try ModelContainer(for: Trip.self)
When reviewing code that uses SwiftData class inheritance, verify each item:
@Model is applied to the base class AND every subclasssuper.init() with all required base properties@Attribute(.preserveValueOnDeletion) is used on fields needed after deletion (sync IDs, audit trails)inverse: parameters correctly, pointing to the base class property@Relationship(deleteRule:) is specified on owning side (.cascade, .nullify, or .deny)as? Subclass) is used safely with if let in views and logicmacos-swiftdata-architecture/swift-concurrency-patterns/generators-persistence-setup/