SwiftData di iOS 26 (Swift 6.2 + Xcode 26) bawa beberapa perubahan penting. Tapi yang paling menonjol adalah ini:
- Class Inheritance: subclass
@Model sekarang bisa diwariskan layaknya class Swift biasa.
- Predicate dengan keyword
is: kamu bisa filter hasil @Query berdasarkan tipe subclass — keren kan?
- Persistent History dengan
sortBy: history fetch sekarang bisa di-sort, sebelumnya nggak bisa (akhirnya!).
- Migration Stage untuk inheritance: lightweight migration dari schema flat ke schema dengan subclass.
Catatan penting: fitur ini hanya tersedia di iOS 26 ke atas. Setiap subclass @Model wajib diberi anotasi @available(iOS 26, *). Jadi kalau minimum deployment target aplikasimu masih di bawah iOS 26, ya, kamu belum bisa pakai fitur ini. Sayang sekali, tapi begitulah.
Kapan Pakai Inheritance, Composition, atau Protocol?
Sebelum buru-buru pakai inheritance, pertimbangkan dulu tiga opsi ini. Saya pernah salah pilih di project lama (akhirnya harus refactor besar-besaran), jadi pilihan awal ini penting banget.
Inheritance — kalau model membentuk hierarki natural
Cocok ketika subclass memang adalah parent-nya, ditambah sedikit perilaku khusus. Contoh klasik: Trip sebagai parent, lalu BusinessTrip dan PersonalTrip sebagai subclass. Semua trip punya destination, startDate, dan endDate; subclass tinggal nambah field spesifik. Sederhana.
Composition — kalau dua model punya bagian yang sama tapi tidak sehirarki
Misal Invoice dan Order sama-sama punya Address. Invoice bukan jenis Order, jadi pakai composition: simpan Address sebagai property terpisah. Selesai.
Protocol Conformance — kalau cuma butuh interface yang sama
Kalau dua model cuma butuh kontrak yang sama (misal sama-sama bisa archive()), pakai protocol. Inheritance bikin physical schema jadi satu tabel besar dengan banyak kolom null — protocol nggak.
Aturan praktisnya: kalau kamu sering melakukan query polimorfik (mengambil semua Trip tanpa peduli subclass), pakai inheritance. Kalau hampir tidak pernah, mending pertimbangkan tabel terpisah dengan protocol.
Membuat Model Inheritance Pertama
Mari mulai dengan parent class Trip yang sederhana:
import SwiftData
import Foundation
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
init(destination: String, startDate: Date, endDate: Date) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
Sekarang kita tambahkan dua subclass dengan field tambahan. Perhatikan baik-baik anotasi @available — ini bukan opsional:
@available(iOS 26, macOS 26, *)
@Model
class BusinessTrip: Trip {
var perdiem: Double = 0.0
var clientName: String = ""
init(destination: String,
startDate: Date,
endDate: Date,
perdiem: Double = 0.0,
clientName: String = "") {
self.perdiem = perdiem
self.clientName = clientName
super.init(destination: destination,
startDate: startDate,
endDate: endDate)
}
}
@available(iOS 26, macOS 26, *)
@Model
class PersonalTrip: Trip {
enum Reason: String, Codable {
case vacation, family, health, other
}
var reason: Reason = Reason.vacation
init(destination: String,
startDate: Date,
endDate: Date,
reason: Reason = .vacation) {
self.reason = reason
super.init(destination: destination,
startDate: startDate,
endDate: endDate)
}
}
Beberapa hal yang wajib kamu perhatikan (dan saya tekankan karena saya pernah lupa salah satunya):
- Setiap subclass harus dianotasi
@available(iOS 26, *). Tanpa ini, kompilasi langsung gagal di project dengan deployment target di bawah iOS 26.
- Property baru di subclass wajib punya default value agar lightweight migration bisa berjalan. Lupa aja sekali, dan kamu bakal ketemu crash di runtime.
- Selalu panggil
super.init(...) di akhir initializer subclass, setelah set property milik subclass.
Mendaftarkan Subclass ke ModelContainer
SwiftData perlu tahu semua tipe (parent dan subclass) yang ada di schema. Jadi, daftarkan semuanya saat membuat ModelContainer:
import SwiftUI
import SwiftData
@main
struct TripApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Trip.self,
BusinessTrip.self,
PersonalTrip.self
])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [config])
} catch {
fatalError("Gagal membuat ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
Query Subclass dengan Keyword is
Nah, ini bagian favorit saya. #Predicate di iOS 26 sekarang mendukung type-check dengan keyword is, persis seperti pattern matching Swift biasa. Artinya? Kamu bisa memfilter berdasarkan subclass langsung dari database — tanpa fetch dulu lalu filter di memori.
Mengambil Semua Trip (Polimorfik)
struct AllTripsView: View {
@Query(sort: \Trip.startDate) private var trips: [Trip]
var body: some View {
List(trips) { trip in
TripRow(trip: trip)
}
}
}
Query di atas mengembalikan semua trip — termasuk BusinessTrip dan PersonalTrip — karena semua subclass otomatis di-fetch saat parent yang di-query. Magis? Bukan, cuma SwiftData yang sudah dewasa.
Filter Hanya Subclass Tertentu
@available(iOS 26, *)
struct BusinessTripsView: View {
@Query(
filter: #Predicate<Trip> { trip in
trip is BusinessTrip
},
sort: \Trip.startDate
)
private var businessTrips: [Trip]
var body: some View {
List(businessTrips) { trip in
if let bt = trip as? BusinessTrip {
VStack(alignment: .leading) {
Text(bt.destination).font(.headline)
Text("Per diem: $\(bt.perdiem, specifier: "%.2f")")
Text("Klien: \(bt.clientName)")
.foregroundStyle(.secondary)
}
}
}
}
}
Compound Predicate: Search + Subclass Filter
Kamu bisa kombinasikan filter subclass dengan filter teks biasa, persis seperti contoh di sesi WWDC25 Apple:
@available(iOS 26, *)
struct SearchableTripsView: View {
@State private var searchText = ""
var body: some View {
TripsList(searchText: searchText)
.searchable(text: $searchText)
}
}
@available(iOS 26, *)
struct TripsList: View {
@Query private var trips: [Trip]
init(searchText: String) {
let predicate = #Predicate<Trip> { trip in
trip is PersonalTrip &&
(searchText.isEmpty || trip.destination.localizedStandardContains(searchText))
}
_trips = Query(filter: predicate, sort: \Trip.startDate)
}
var body: some View {
List(trips) { trip in
TripRow(trip: trip)
}
}
}
Predicate digabung dengan operator &&, dan SwiftData akan menerjemahkan ini ke query SQLite yang efisien — tanpa harus fetch semua data lalu filter di memori. Ini perbedaan besar untuk dataset yang gede.
Migrasi Schema: Dari Tanpa Inheritance ke Inheritance
Aplikasi yang sudah live nggak bisa langsung pakai inheritance — kamu butuh VersionedSchema dan SchemaMigrationPlan. Berikut polanya kalau aplikasimu sebelumnya punya schema flat (V3) dan ingin pindah ke schema dengan subclass (V4).
Versi 3.0 — Schema Flat (Pre iOS 26)
enum TripSchemaV3: VersionedSchema {
static var versionIdentifier = Schema.Version(3, 0, 0)
static var models: [any PersistentModel.Type] { [Trip.self] }
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
var category: String // "business" atau "personal"
init(destination: String, startDate: Date, endDate: Date, category: String) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
self.category = category
}
}
}
Versi 4.0 — Schema dengan Inheritance
@available(iOS 26, *)
enum TripSchemaV4: VersionedSchema {
static var versionIdentifier = Schema.Version(4, 0, 0)
static var models: [any PersistentModel.Type] {
[Trip.self, BusinessTrip.self, PersonalTrip.self]
}
@Model
class Trip {
var destination: String
var startDate: Date
var endDate: Date
init(destination: String, startDate: Date, endDate: Date) {
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
@Model
class BusinessTrip: Trip {
var perdiem: Double = 0.0
// ... initializer
}
@Model
class PersonalTrip: Trip {
var reason: String = "vacation"
// ... initializer
}
}
Migration Plan
enum TripMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
if #available(iOS 26, *) {
return [TripSchemaV3.self, TripSchemaV4.self]
} else {
return [TripSchemaV3.self]
}
}
static var stages: [MigrationStage] {
if #available(iOS 26, *) {
return [migrateV3toV4]
} else {
return []
}
}
@available(iOS 26, *)
static let migrateV3toV4 = MigrationStage.custom(
fromVersion: TripSchemaV3.self,
toVersion: TripSchemaV4.self,
willMigrate: { context in
// Field "category" akan di-drop; data lama tetap jadi Trip biasa.
// Kalau mau, kamu bisa konversi manual data lama jadi subclass:
let trips = try context.fetch(FetchDescriptor<TripSchemaV3.Trip>())
for old in trips {
_ = old.category
}
try context.save()
},
didMigrate: nil
)
}
Pasang Migration Plan ke ModelContainer
let container = try ModelContainer(
for: Trip.self, BusinessTrip.self, PersonalTrip.self,
migrationPlan: TripMigrationPlan.self
)
Untuk perubahan sederhana — misal cuma menambahkan subclass tanpa mengubah field parent — kamu bisa pakai MigrationStage.lightweight sebagai gantinya, tanpa closure willMigrate. Lebih praktis, lebih hemat kode.
Bekerja dengan Relationship di Subclass
Subclass juga bisa punya relationship sendiri. Contoh: hanya BusinessTrip yang punya daftar Expense (karena ya, masa PersonalTrip ngitung expense tagihan klien?):
@available(iOS 26, *)
@Model
class Expense {
var amount: Double
var note: String
var trip: BusinessTrip?
init(amount: Double, note: String) {
self.amount = amount
self.note = note
}
}
@available(iOS 26, *)
@Model
class BusinessTrip: Trip {
var perdiem: Double = 0.0
@Relationship(deleteRule: .cascade, inverse: \Expense.trip)
var expenses: [Expense]? = []
init(destination: String, startDate: Date, endDate: Date, perdiem: Double = 0.0) {
self.perdiem = perdiem
super.init(destination: destination, startDate: startDate, endDate: endDate)
}
}
SwiftData otomatis tahu kalau hanya BusinessTrip yang punya relasi ke Expense; PersonalTrip dan Trip dasar nggak terpengaruh sama sekali.
Praktik Terbaik dan Kendala Umum
1. Selalu beri default value pada property baru
Tanpa default, lightweight migration bakal gagal — dan kamu terpaksa pakai custom migration yang jauh lebih ribet. Percayalah, hindari aja.
2. Hindari hierarki yang terlalu dalam
SwiftData menyimpan inheritance di satu tabel besar (single-table inheritance). Hierarki 3+ level bikin tabel sangat lebar dengan banyak kolom null. Maksimal 2 level cukup untuk hampir semua kasus realistis.
3. Gunakan @available di setiap titik
Bukan cuma di subclass, tapi juga di VersionedSchema, di view yang pakai subclass, dan di property yang merujuk subclass. Compiler Swift bakal kasih tahu kalau ada yang kelewat — dengarkan dia.
4. Test migration dengan data dummy
Sebelum rilis ke App Store, install versi V3 di simulator, isi data dummy, lalu update ke build dengan V4. Pastikan data lama nggak hilang dan tidak ada crash. Saya selalu wajibkan ini di tim saya, dan beberapa kali itu nyelametin kami dari bug yang nyebelin di production.
5. Hati-hati dengan #Predicate dan optional
Saat cast ke subclass di body view (trip as? BusinessTrip), pastikan kamu handle case nil. Kombinasikan dengan filter is di predicate supaya cast jarang gagal.
Bug yang Perlu Diwaspadai di iOS 26.0 / 26.1
Per Mei 2026, ada beberapa laporan bug terkait inheritance + relationship dengan deleteRule: .cascade di forum Apple Developer. Workaround sementara: jangan pasang cascade pada relasi dari subclass ke entity lain yang juga punya inheritance — pisahkan dulu jadi composition sederhana sampai bug ter-fix di iOS 26.2. Selalu update ke patch terbaru sebelum rilis, oke?
Pertanyaan yang Sering Ditanyakan (FAQ)
Apakah SwiftData inheritance bisa dipakai di iOS 18 atau iOS 17?
Tidak. Fitur ini eksklusif iOS 26 ke atas. Subclass @Model harus dianotasi @available(iOS 26, *), dan kalau deployment target aplikasi di bawah itu, kompilasi bakal gagal. Untuk multi-target, gunakan flag #available dan sediakan jalur fallback dengan composition atau enum-tagged single class.
Apa bedanya inheritance dengan composition di SwiftData?
Inheritance bikin satu tabel dengan kolom dari parent + semua subclass (kolom subclass jadi null kalau row-nya parent atau subclass lain). Composition bikin tabel terpisah dengan relasi. Pakai inheritance kalau kamu sering query polimorfik; pakai composition kalau model tidak benar-benar punya hubungan "is-a".
Bisakah saya melakukan query fetch langsung pada subclass tanpa @Query?
Bisa. Pakai FetchDescriptor<BusinessTrip> untuk hanya mengambil instance subclass tertentu, atau FetchDescriptor<Trip> dengan predicate berisi trip is BusinessTrip untuk hasil yang sama lewat parent.
Apakah CloudKit sync mendukung model inheritance?
Ya, tapi dengan catatan: semua subclass yang akan disinkronkan harus punya default value untuk semua property non-optional, dan tidak boleh ada relationship yang required. Ini sama persis dengan persyaratan CloudKit standar di SwiftData.
Bagaimana cara migrasi data lama (yang punya field category) menjadi subclass yang sesuai?
Pakai custom MigrationStage. Di closure willMigrate, fetch semua data versi lama, baca field discriminator (misal category), buat instance subclass yang sesuai, lalu hapus instance lama. Setelah migrasi selesai, schema baru tinggal pakai inheritance native.
Kesimpulan
SwiftData Model Inheritance di iOS 26 menutup salah satu gap terbesar framework ini sejak diperkenalkan di WWDC23. Dengan dukungan subclass @Model, predicate is, dan migration plan yang jelas, kamu sekarang bisa membangun arsitektur data yang bersih — tanpa harus pakai trick enum-discriminator atau tabel paralel.
Saran saya? Mulailah dengan kasus sederhana dulu — misal mengubah satu model flat jadi parent + 2 subclass. Test migrasinya secara menyeluruh, naikkan deployment target ke iOS 26 hanya kalau audiens app-mu memang sudah siap, dan nikmati query polimorfik yang sebelumnya nggak mungkin tanpa Core Data.
Selamat ngoding, dan semoga sukses dengan project SwiftData-mu!