SwiftData’yı 2 projede production’a aldım. Demo tutorial’larında güzel görünen API, gerçek veri hacminde farklı tavırlar sergiliyor. Özellikle relationship yönetimi ve schema migration konularında öğrendiklerim var.
Relationship yönetimi: cascade ve inverse
İlk projede parent-child ilişkisini sadece bir tarafta tanımladım. Delete edince orphan record’lar kaldı. Çözüm inverse relationship’i mutlaka belirtmek:
@Model
final class Workout {
@Relationship(deleteRule: .cascade, inverse: Exercise.workout)
var exercises: [Exercise] = []
}
@Model
final class Exercise {
var workout: Workout?
}Inverse belirtilmezse SwiftData ilişkiyi tek yönlü kuruyor ve cascade delete çalışmıyor. Core Data’dan gelenler için tanıdık ama SwiftData’nın daha tolere edici bir yapısı var gibi görünüyor; sonra fark ediyorsun.
To-many relationship ve fetch performansı
Bir to-many ilişkisinde @Relationship varsayılan olarak lazy load yapıyor. Listede 500 workout gösterdiğim ekranda her hücre kendi exercise count’unu sorgulayınca UI donmaya başladı.
struct WorkoutRow: View {
let workout: Workout
var body: some View {
HStack {
Text(workout.name)
Spacer()
Text("(workout.exercises.count)")
}
}
}Bu kod 500 satırda 500 ayrı fetch tetikliyor. Çözüm için ya exerciseCount‘u cached property olarak modele ekliyorsun ya da FetchDescriptor’da relationshipKeyPathsForPrefetching kullanıyorsun. Ben ikinci yolu tercih ediyorum:
var descriptor = FetchDescriptor<Workout>()
descriptor.relationshipKeyPathsForPrefetching = [.exercises]Schema migration: lightweight ve custom
Version bump’larda iki migration tipi var. Lightweight, property ekleme/çıkarma gibi basit değişikliklerde otomatik çalışıyor. Custom migration ise veri dönüşümü gerektiğinde devreye giriyor.
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Workout.self] }
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] { [Workout.self] }
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// pre-migration cleanup
},
didMigrate: { context in
// data transformation
}
)
}Bir field’ın enum’a dönüştürülmesi
Eski schema’da status: String tutuyordum. Yeni schema’da bunu enum yapmak istedim. Lightweight migration bu dönüşümü kaldırmıyor çünkü storage tipi değişiyor. Custom stage’de didMigrate closure’ında tüm kayıtları iterate edip string’i enum’a mapliyorum.
Önemli not: custom migration içinde eski property’e erişmek için eski schema modelini kullanman lazım. SwiftData bunu fromVersion ve toVersion tipleri üzerinden yapıyor.
ModelContainer kurulumu: production ayarları
let schema = Schema([Workout.self, Exercise.self])
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
cloudKitDatabase: .automatic
)
let container = try ModelContainer(
for: schema,
migrationPlan: AppMigrationPlan.self,
configurations: [config]
)CloudKit sync istiyorsan tüm property’lerin optional veya default değere sahip olması gerekiyor. Zorunlu bir alanın varsa sync reddedilir ve konteynır açılırken crash olursun.
Test stratejim
Her migration için XCTest’te in-memory ModelContainer oluşturup, eski schema ile seed data yazıp, yeni schema’ya geçiyorum. Veri dönüşümünün doğruluğunu assert ediyorum. Ayrıca TestFlight’a gönderip eski uygulamayı yükleyip üzerine güncellemeyi test ediyorum; bu adım kritik çünkü production cihazda beklemediğin edge case’ler çıkıyor.
Hangi durumda Core Data’da kalırım
Tamamen hayali kullanıcı tabanı yok, mevcut bir app’te ağır Core Data kodu varsa; NSFetchedResultsController ve çok karmaşık predicate kullanımı varsa; performance profile’da belli kısımları elle tune ettiysen. SwiftData henüz Core Data’nın tüm granular kontrolünü vermiyor.
Son notlar
SwiftData yeni proje için iyi bir seçim. Mevcut Core Data projesini taşımak için önce mantıklı bir sebep aramalısın. Taşıma işi 2 haftadan az bir iş değil, tam test coverage yoksa 2 ayı bulabilir. Migration’u custom yazmaya hazır olmak lazım; Apple’ın lightweight promise’i çoğu gerçek senaryoda yetmiyor.