• 18.08.2026 09:06:16
  • Admin Admin

SwiftData kullanan uygulamalarda şema değişikliklerini veri kaybetmeden yönetin. Bu iOS eğitimi düzeyindeki rehber; VersionedSchema, migration testleri, Instruments ölçümü ve App Store sürüm atlama senaryolarını ele alır.

SwiftData Şema Evrimi: iOS MVVM Mimarisinde Güvenli Veri Göçü

iOS eğitimi için kritik konu: Şema değişikliği bir release sözleşmesidir

SwiftData modeline alan eklemek, bir property'yi zorunlu yapmak veya ilişkiyi değiştirmek yalnızca derleme zamanı değişikliği değildir: cihazdaki SQLite store'un metadatası ile çalıştırdığınız Schema arasında bir sözleşme oluşturur. Bir ios eğitimi veya swift eğitimi müfredatında bu konu genellikle atlanır; oysa gerçek kullanıcı eski sürümden doğrudan birkaç sürüm sonraki sürüme geçebilir. Bu nedenle migration plan'ı yalnızca V(n-1) → Vn değil, desteklediğiniz en eski store → güncel store yolunu test etmelidir.

Örneğin yer imi uygulamasında URL'nin host bilgisini sonradan eklemek istiyorsanız, ilk geçişte alanı nullable tanımlayın. Eski satırlarda değer bulunmadığı için String gibi non-optional bir alanı doğrudan eklemek, store açılırken migration'ın başarısız olmasına veya özel dönüşüm gerektirmesine neden olur. Nullable alanı uygulama açıldıktan sonra backfill etmek, veri dönüşümü ile şema dönüşümünü birbirinden ayırır.

import SwiftData

extension BookmarkSchemaV1 {
    @Model
    final class Bookmark {
        @Attribute(.unique) var url: String
        var title: String
        var createdAt: Date

        init(url: String, title: String, createdAt: Date = .now) {
            self.url = url
            self.title = title
            self.createdAt = createdAt
        }
    }
}

extension BookmarkSchemaV2 {
    @Model
    final class Bookmark {
        @Attribute(.unique) var url: String
        var title: String
        var createdAt: Date
        var sourceHost: String? // V1 kayıtları için bilinçli olarak optional

        init(url: String, title: String, createdAt: Date = .now, sourceHost: String? = nil) {
            self.url = url
            self.title = title
            self.createdAt = createdAt
            self.sourceHost = sourceHost
        }
    }
}

Yaygın hata, URL'yi normalize etmeden @Attribute(.unique) ile benzersiz sanmaktır. https://example.com ve https://example.com/ farklı string'lerdir; migration sonrasında aynı kaydı iki kez üretirsiniz. Yazma sınırında canonical string üretin: URLComponents ile host'u lowercase yapın, varsayılan portu kaldırın ve path boşsa / kullanın. Bu normalizasyonu modelin didSet'ine koymak yerine repository/actor yazma metodunda tutmak daha güvenlidir; SwiftData migration sırasında property observer'larının çalışacağı varsayımıyla tasarım yapılmamalıdır.

SwiftUI eğitimi kapsamında VersionedSchema ve migration plan kurulumu

SwiftData'nın otomatik olarak taşıyabildiği eklemeler için bile şemaları isimlendirmek gerekir; aksi halde ileride hangi değişikliğin store'u bozduğunu kaynak koddan ayırmak zorlaşır. Aşağıdaki yapı, V1'den V2'ye lightweight migration tanımlar ve container'ı açıkça bu planla oluşturur. Bu kod, swiftui eğitimi sırasında .modelContainer(for:) kısayolundan önce öğrenilmelidir; çünkü kısayol migration plan'ını ifade edecek yer bırakmaz.

import SwiftData

enum BookmarkSchemaV1: VersionedSchema {
    static var versionIdentifier: Schema.Version { .init(1, 0, 0) }
    static var models: [any PersistentModel.Type] {
        [BookmarkSchemaV1.Bookmark.self]
    }
}

enum BookmarkSchemaV2: VersionedSchema {
    static var versionIdentifier: Schema.Version { .init(2, 0, 0) }
    static var models: [any PersistentModel.Type] {
        [BookmarkSchemaV2.Bookmark.self]
    }
}

enum BookmarkMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [BookmarkSchemaV1.self, BookmarkSchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [.lightweight(fromVersion: BookmarkSchemaV1.self,
                      toVersion: BookmarkSchemaV2.self)]
    }
}

@main
struct ReaderApp: App {
    let container: ModelContainer = {
        try! ModelContainer(
            for: BookmarkSchemaV2.Bookmark.self,
            migrationPlan: BookmarkMigrationPlan.self
        )
    }()

    var body: some Scene {
        WindowGroup { BookmarkListView() }
            .modelContainer(container)
    }
}

Bir sonraki uygulama açılışında eksik sourceHost değerlerini ayrı bir iş olarak doldurun. Bu iş idempotent olmalı: yalnızca nil kayıtları fetch edilir; uygulama işlem ortasında kapanırsa bir sonraki açılış kalan kayıtları sürdürür. Büyük store'larda tüm nesneleri belleğe çekmeyin; FetchDescriptor için fetchLimit ve fetchOffset ile partiler kullanın. Her partiden sonra save() çağırmak, tek dev transaction'ın WAL dosyasını gereksiz büyütmesini önler.

iOS MVVM mimarisi: ModelContext'i ViewModel'e sızdırmadan taşıma

ios mvvm mimarisi içinde @Query küçük ekranlar için pratik olsa da, import, deduplikasyon ve backfill işlerini ViewModel'e koymak test edilebilirliği düşürür. Ayrıca @ModelSendable bir DTO değildir. Kalıcı erişimi @ModelActor ile sınırlayın, UI'a yalnızca değer tipleri döndürün.

import Foundation
import SwiftData

struct BookmarkDTO: Identifiable, Sendable {
    let id: PersistentIdentifier
    let title: String
    let url: String
    let sourceHost: String?
}

@ModelActor
actor BookmarkStore {
    func missingHosts(limit: Int = 200) throws -> [BookmarkDTO] {
        var descriptor = FetchDescriptor<BookmarkSchemaV2.Bookmark>(
            predicate: #Predicate { $0.sourceHost == nil }
        )
        descriptor.fetchLimit = limit

        return try modelContext.fetch(descriptor).map {
            BookmarkDTO(id: $0.persistentModelID,
                        title: $0.title,
                        url: $0.url,
                        sourceHost: $0.sourceHost)
        }
    }

    func setHost(_ host: String?, for id: PersistentIdentifier) throws {
        guard let bookmark = modelContext.model(for: id) as? BookmarkSchemaV2.Bookmark else {
            return // Kayıt başka işlemde silinmiş olabilir.
        }
        bookmark.sourceHost = host
        try modelContext.save()
    }
}

ViewModel, Task içinde DTO listesini alıp host çözümlemesini yapabilir; değişiklikleri tekrar actor'a gönderir. Özellikle model(for:) sonucunun silinmiş bir nesne olabileceğini kontrol edin: migration/backfill çalışırken kullanıcı aynı kaydı silebilir. Bu edge case'i atlamak, zor yeniden üretilebilen 'deleted object' hatalarına yol açar. Bu ayrım, bir swift kursu projesinde mock repository ile backfill mantığını SQLite açmadan unit test etmeyi de mümkün kılar.

Xcode eğitimi: Migration süresini Instruments ile ölçme ve sınır koyma

Migration başlangıç maliyetini tahmin ederek değil, üretim boyutuna yakın fixture store ile ölçün. xcode eğitimi pratiğinde os_signpost ekleyin; ardından Xcode Instruments'ta Points of Interest şablonunu açıp aynı cihazda önce eski yaklaşımı, sonra batch'li yaklaşımı kaydedin. Karşılaştırmada toplam migration/backfill süresi yanında ilk frame'e kadar geçen zamanı ve peak memory'yi Memory Graph veya Allocations ile not edin.

import os

let migrationLog = OSSignposter(
    subsystem: "com.opendart.reader",
    category: "SwiftDataMigration"
)

func backfillHosts(store: BookmarkStore) async {
    let state = migrationLog.beginInterval("backfill-hosts")
    defer { migrationLog.endInterval("backfill-hosts", state) }

    while let batch = try? await store.missingHosts(limit: 200), !batch.isEmpty {
        for bookmark in batch {
            let host = URL(string: bookmark.url)?.host?.lowercased()
            try? await store.setHost(host, for: bookmark.id)
        }
    }
}

Önce/sonra deneyinde aynı 10.000 kayıtlı V1 fixture'ı kullanın. Örneğin önce tek seferde fetch edip tüm kayıtları kaydeden sürümü, sonra 200'lük batch sürümünü ölçün; yalnızca 'daha hızlı' sonucunu değil, signpost interval'ının medyanını ve Allocations'taki en yüksek canlı nesne sayısını kaydedin. while döngüsünde host'u çözülemeyen URL için nil yazarsanız kayıt tekrar tekrar seçilir ve sonsuz döngü oluşur. Bunu önlemek için geçersiz URL'lere ayrı bir durum alanı yazın veya predicate'i işlenemeyen kayıtları dışlayacak şekilde tasarlayın.

iPhone uygulama geliştirme ve App Store yayınlama öncesi migration testi

iphone uygulama geliştirme sürecinde migration testinin girdisi boş in-memory container değil, önceki uygulama sürümünün oluşturduğu gerçek store olmalıdır. CI'da bir test target'ı V1 şemasıyla fixture üretmeli, container'ı kapatmalı, ardından V2 planıyla aynı dosyayı açmalıdır. Test, kayıt sayısını, benzersiz URL'leri ve beklenen nullable alanları doğrulamalıdır. Böylece modelde yapılan görünüşte masum bir rename'in store açılışını bozduğunu release'ten önce yakalarsınız.

func testV1StoreOpensWithV2Plan() throws {
    let url = URL.temporaryDirectory.appending(path: "migration-test.store")
    defer { try? FileManager.default.removeItem(at: url) }

    let v1Config = ModelConfiguration(url: url)
    let v1 = try ModelContainer(for: BookmarkSchemaV1.Bookmark.self,
                                configurations: v1Config)
    let writer = ModelContext(v1)
    writer.insert(BookmarkSchemaV1.Bookmark(url: "https://example.com", title: "Example"))
    try writer.save()

    let v2Config = ModelConfiguration(url: url)
    let v2 = try ModelContainer(for: BookmarkSchemaV2.Bookmark.self,
                                migrationPlan: BookmarkMigrationPlan.self,
                                configurations: v2Config)
    let reader = ModelContext(v2)
    let rows = try reader.fetch(FetchDescriptor<BookmarkSchemaV2.Bookmark>())

    XCTAssertEqual(rows.count, 1)
    XCTAssertEqual(rows[0].url, "https://example.com")
    XCTAssertNil(rows[0].sourceHost)
}

app store yayınlama aşamasında kullanıcıların ara sürümleri atlayacağını varsayın: V1 kullanan bir cihaz doğrudan V4'e güncellenebilir. Bu yüzden CI matrisi yalnızca komşu şemaları değil, destek politikanızdaki her eski fixture → güncel plan kombinasyonunu açmalıdır. Store açma hatasında uygulamayı otomatik silip yeniden yaratmak yerel kullanıcı verisini yok eder; bunun yerine hata kodunu, store metadata sürümünü ve migration stage bilgisini kişisel veri içermeden telemetry aracınıza kaydedin. Bu kayıtlar, rollout durdurma kararını gerçek cihaz verisine dayandırır.

Sık Sorulan Sorular

SwiftData migration iOS MVVM mimarisi içinde nerede çalıştırılmalı?

Şema migration'ı ModelContainer açılırken çalışır; uygulama seviyesinde container oluşturun. Veri backfill işlemlerini ise @ModelActor içindeki repository'ye koyun. ViewModel yalnızca Sendable DTO almalı; @Model örneğini actor veya MainActor sınırı dışına taşımamalıdır.

SwiftUI eğitimi projelerinde SwiftData'ya zorunlu alan nasıl eklenir?

İlk şemada yeni alanı optional ekleyip lightweight migration kullanın. Sonraki açılışta nil kayıtlarını FetchDescriptor ve fetchLimit ile partiler halinde doldurun. Geçersiz girdilerin sürekli yeniden seçilmesini önlemek için işleme durumu tutun veya predicate'i buna göre daraltın.

Xcode eğitimi sırasında SwiftData migration performansı nasıl ölçülür?

Kodda OSSignposter ile migration ve backfill interval'larını işaretleyin; Instruments Points of Interest ile aynı fixture store üzerinde önce/sonra sürelerini ölçün. Allocations ile peak bellek değerini de karşılaştırın. Boş store yerine örneğin 10.000 satırlık eski şema fixture'ı kullanın.

App Store yayınlama öncesinde SwiftData migration için hangi test zorunludur?

Önceki şemayla disk üzerinde store oluşturan, container'ı kapatan ve güncel SchemaMigrationPlan ile aynı URL'den yeniden açan XCTest yazın. Kayıt sayısı, kritik alanlar ve benzersizlik kuralları doğrulanmalı; test matrisi en eski desteklenen store'dan güncel şemaya doğrudan geçişi de içermelidir.

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.

Opendart Akademi llms.txt