SwiftUI eğitiminde SwiftData şema migrasyonu, CloudKit kısıtları, çatışma çözümü ve gerçek cihaz doğrulamasını ele alın. Sürüm yükseltmelerinde veri kaybını önleyen uygulanabilir bir yayın akışı kurun.
SwiftUI Eğitiminde SwiftData Şema Migrasyonu ve CloudKit Senkronu
SwiftUI eğitimi için migrasyon: Şemayı ilk günden sürümleyin
Bir ios eğitimi veya swift eğitimi içinde SwiftData'yı yalnızca @Model ile başlatmak yetersizdir: uygulamanın mağazaya çıkan ilk kalıcı modeli, sonraki tüm sürümlerin veri sözleşmesidir. Xcode'da model alanına sonradan zorunlu ve varsayımsız bir özellik eklemek, eski SQLite deposundaki satırlarda karşılığı olmadığı için container açılışını bozabilir. Bu nedenle ilk sürümden itibaren VersionedSchema ve SchemaMigrationPlan tanımlayın; sürüm kimliklerini geriye dönük değiştirmeyin.
import SwiftData
enum SchemaV1: VersionedSchema {
static var versionIdentifier: Schema.Version { .init(1, 0, 0) }
static var models: [any PersistentModel.Type] { [Note.self] }
@Model
final class Note {
var id: UUID
var title: String
var createdAt: Date
init(title: String) {
self.id = UUID()
self.title = title
self.createdAt = .now
}
}
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier: Schema.Version { .init(2, 0, 0) }
static var models: [any PersistentModel.Type] { [Note.self] }
@Model
final class Note {
var id: UUID
var title: String
var createdAt: Date
var pinnedAt: Date?
init(title: String, pinnedAt: Date? = nil) {
self.id = UUID()
self.title = title
self.createdAt = .now
self.pinnedAt = pinnedAt
}
}
}
enum NoteMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] }
static var stages: [MigrationStage] {
[ .lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self) ]
}
}Buradaki kritik ayrıntı pinnedAt alanının Date? olmasıdır. Lightweight migration, mevcut kayıtlara bu alan için nil yazabilir; Date gibi zorunlu bir alanın güvenli varsayılanı iş kuralına bağlıdır ve otomatik üretildiğini varsaymak veri anlamını değiştirebilir. Uygulama container'ını tek bir composition root'ta planla oluşturun; görünüm içinde tekrar tekrar ModelContainer yaratmak farklı store örnekleri ve testte görünmeyen veri davranışları üretir.
import SwiftUI
import SwiftData
@main
struct NotesApp: App {
let container: ModelContainer
init() {
let schema = Schema(versionedSchema: SchemaV2.self)
let configuration = ModelConfiguration(
"NotesStore",
schema: schema,
isStoredInMemoryOnly: false
)
do {
container = try ModelContainer(
for: schema,
migrationPlan: NoteMigrationPlan.self,
configurations: [configuration]
)
} catch {
fatalError("Persistent store cannot open: \(error)")
}
}
var body: some Scene {
WindowGroup { NoteListView() }
.modelContainer(container)
}
}iOS MVVM mimarisi ile SwiftData modelini ekran durumundan ayırın
ios mvvm mimarisi uygularken SwiftData @Model nesnesini doğrudan form ekranının geçici durumu olarak kullanmak, kullanıcı Kaydet'e basmadan store'a yazılmasına yol açar. Özellikle TextField binding'i model özelliğine bağlıysa her karakter değişimi transaction içine girer, iptal düğmesi ise eski değeri geri üretmek zorunda kalır. Bunun yerine view model içinde bir draft struct taşıyın, doğrulamayı burada yapın ve yalnızca açık bir save() eyleminde model'i değiştirin.
import SwiftData
struct NoteDraft {
var title = ""
var pinned = false
}
@MainActor
@Observable
final class NoteEditorViewModel {
var draft: NoteDraft
private let note: SchemaV2.Note
private let context: ModelContext
init(note: SchemaV2.Note, context: ModelContext) {
self.note = note
self.context = context
self.draft = NoteDraft(
title: note.title,
pinned: note.pinnedAt != nil
)
}
func save() throws {
let normalized = draft.title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !normalized.isEmpty else { throw ValidationError.emptyTitle }
guard normalized.count <= 140 else { throw ValidationError.titleTooLong }
note.title = normalized
note.pinnedAt = draft.pinned ? (note.pinnedAt ?? .now) : nil
try context.save()
}
}
enum ValidationError: Error { case emptyTitle, titleTooLong }Bu ayrım iphone uygulama geliştirme sürecinde testleri de belirginleştirir. ViewModel'i in-memory ModelContainer ile başlatıp save() sonrasında fetch yapın. Sadece note.title değerini assert etmek yerine, boş başlığın context.save() çağrısından önce reddedildiğini ve pinnedAt değerinin kullanıcı ikinci kez kaydettiğinde değişmediğini test edin. Bu ikinci kontrol önemlidir: her save işleminde pinnedAt = .now yazmak, sıralama ekranında sabitlenmiş notların sırasını fark edilmeden değiştirir.
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: SchemaV2.Note.self, configurations: config)
let context = ModelContext(container)
let note = SchemaV2.Note(title: "İlk not")
context.insert(note)
try context.save()
let vm = NoteEditorViewModel(note: note, context: context)
vm.draft = NoteDraft(title: " Plan ", pinned: true)
try vm.save()
let firstPinnedAt = note.pinnedAt
try vm.save()
assert(note.title == "Plan")
assert(note.pinnedAt == firstPinnedAt)SwiftData ve CloudKit senkronunda model kısıtlarını yayın öncesi denetleyin
CloudKit destekli bir store'a geçişte en sık hata, yalnızca yerel SQLite'ta çalışan bir model varsaymaktır. CloudKit şema üretimi için ilişkilerdeki zorunluluk, benzersizlik ve silme kuralları Apple'ın senkronizasyon modelinin desteklediği biçimde olmalıdır. Örneğin cihazlar arası senkronlanan bir ilişkiyi zorunlu tanımlamak yerine opsiyonel tasarlayıp uygulama katmanında doğrulayın; uzak cihazda ilişkili kaydın henüz indirilmemiş olması geçici olarak eksik ilişki yaratabilir.
let cloudConfig = ModelConfiguration(
"NotesStore",
schema: Schema(versionedSchema: SchemaV2.self),
cloudKitDatabase: .private("iCloud.com.example.notes")
)
let container = try ModelContainer(
for: Schema(versionedSchema: SchemaV2.self),
migrationPlan: NoteMigrationPlan.self,
configurations: [cloudConfig]
)Geliştirme ortamında CloudKit Dashboard'da Development şemasını kontrol ettikten sonra en az iki gerçek cihazda şu senaryoyu çalıştırın: A cihazında notu oluşturun, B cihazında indirildiğini doğrulayın, B'de başlığı değiştirin ve A'yı ağdan çıkarın, A'da farklı başlık girin, sonra ağı geri açın. Beklenen son değeri ürün kararı olarak yazılı hale getirin. CloudKit alan bazında değişiklik birleştirme garantisi vermez; aynı scalar alan için son yazanın kazanması iş gereksiniminizle çelişiyorsa title yerine değişmez revision kayıtları veya sunucu tarafı bir çözüm gerekir.
Bu noktada swift kursu ve xcode eğitimi materyallerinde sık atlanan incelik, Development CloudKit ortamındaki başarılı denemenin Production ortamına otomatik taşınmamasıdır. Dağıtım öncesi CloudKit Dashboard'da şema değişikliklerini Production'a deploy edin, uygulamanın entitlement dosyasındaki iCloud container kimliğini hedefin Signing and Capabilities ekranıyla karşılaştırın ve arşivden çıkan .app içindeki entitlements'ı şu komutla doğrulayın:
codesign -d --entitlements :- "Notes.app"App Store yayınlama öncesi migrasyon ve sorgu maliyetini ölçün
app store yayınlama aşamasında migrasyonun yalnızca temiz kurulumda çalışması yeterli kanıt değildir. Test planınıza önceki sürümün gerçekçi store dosyasıyla yükseltme senaryosu ekleyin. Xcode scheme'inde Test Action için ayrı bir launch argument tanımlayın, uygulama bu argüman aktifken eski fixture store'u Application Support dizinine kopyalasın, sonra yeni ModelContainer'ı açsın. Böylece CI üzerinde hem container açılış hatasını hem de eski kayıt sayısının korunmasını assert edebilirsiniz.
let descriptor = FetchDescriptor<SchemaV2.Note>(
predicate: #Predicate { $0.pinnedAt != nil },
sortBy: [SortDescriptor(\.pinnedAt, order: .reverse)]
)
let pinnedNotes = try context.fetch(descriptor)
assert(pinnedNotes.allSatisfy { $0.pinnedAt != nil })Liste büyüdüğünde önce Xcode Instruments içindeki SwiftData veya Core Data template'i ile fetch sayısını, fault sayısını ve save transaction süresini kaydedin. Ardından ekran açılışında tüm notları çekip Swift'te filter/sorted uygulayan kodu yukarıdaki predicate ve sort descriptor ile değiştirin. Aynı cihaz, aynı fixture ve aynı kullanıcı akışında önce-sonra ölçümü alın: örneğin 10.000 kayıtta fetch sayısı, ana iş parçacığındaki save süresi ve ekrana ilk sonuç gelene kadar geçen süre. Mekanizma şudur: predicate SQLite katmanına iner, uygulama bütün satırları materialize edip Swift closure'larına taşımak yerine yalnızca eşleşen satırları alır.
Bir edge case daha vardır: pinnedAt alanı nullable olduğu için sıralama sonucu nil değerlerinin konumu store davranışına bağlı olarak ürün beklentinizden farklı görünebilir. 'Sabitlenenler üstte' gereksiniminde tek sıralama yerine iki FetchDescriptor kullanmak veya kalıcı bir sortRank alanı tanımlamak daha denetlenebilirdir. Release Candidate'i TestFlight'a yüklemeden önce düşük disk alanı, uçak modu ve eski store fixture'ı ile cold launch ölçümünü tekrar çalıştırın; migrasyon hataları çoğu zaman yalnızca mevcut kullanıcı verisi üzerinde ortaya çıkar.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
SwiftUI eğitiminde SwiftData'ya yeni zorunlu alan nasıl eklenir?
Önce VersionedSchema ile eski ve yeni modeli ayrı tanımlayın. Eski kayıtlarda değer üretilebilecek anlamlı bir varsayılan yoksa yeni alanı önce optional ekleyin ve lightweight migration kullanın. Zorunlu alana doğrudan geçmek gerekiyorsa MigrationStage.custom içinde eski kayıtlara iş kuralına uygun değer yazın; ardından eski store fixture'ı ile container açılışını CI testinde doğrulayın.
iOS MVVM mimarisi içinde SwiftData modelini doğrudan TextField binding'ine vermeli miyim?
İptal edilebilir düzenleme ekranında vermeyin. @Model yerine NoteDraft gibi bir value type'ı view model'de tutun, trim ve uzunluk doğrulamasını save() içinde yapın, ardından tek bir context.save() çağrısı kullanın. Bu yaklaşım her tuş vuruşunun kalıcı transaction olmasını ve Cancel sonrasında geri alma kodu yazılmasını önler.
iPhone uygulama geliştirme sürecinde CloudKit SwiftData senkronu nasıl test edilir?
İki fiziksel cihaz ve aynı iCloud hesabıyla oluşturma, karşı cihaza indirme, çevrimdışı eşzamanlı düzenleme ve yeniden bağlanma akışını çalıştırın. Aynı alan iki cihazda değiştiğinde beklenen çözümü ürün gereksinimi olarak belirleyin. Ayrıca CloudKit Dashboard'da Production şemasını deploy edin ve codesign -d --entitlements :- komutuyla arşivdeki container entitlement'ını kontrol edin.
Swift kursu projelerinde SwiftData sorgu maliyeti nasıl ölçülür?
Xcode Instruments'ta SwiftData veya Core Data template'i ile ekran açılışındaki fetch sayısını ve save transaction sürelerini kaydedin. Sonra Swift tarafındaki filter ve sorted zincirlerini FetchDescriptor predicate ile sortBy'a taşıyın. Aynı fixture store ve aynı cihazla önce-sonra karşılaştırması yapmadan sorgu değişikliğini performans iyileştirmesi olarak kabul etmeyin.
AI / LLM Discovery
Bu makale Opendart Akademi iOS / Swift eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.


