SwiftData در iOS 26: راهنمای کامل وراثت مدل، مهاجرت اسکیما و CloudKit

راهنمای عملی SwiftData در iOS 26: از وراثت مدل و مهاجرت اسکیما تا کوئری‌های پیشرفته با FetchDescriptor، عملیات پس‌زمینه با @ModelActor و همگام‌سازی CloudKit همراه با مثال‌های کاربردی.

مقدمه: SwiftData و تحول مدیریت داده در iOS 26

وقتی اپل در سال ۲۰۲۳ همراه با iOS 17 فریمورک SwiftData رو معرفی کرد، خیلی‌ها هیجان‌زده شدند — بالاخره یه جایگزین مدرن و بومی برای Core Data داشتیم. ولی راستش رو بخواید، SwiftData تو اون سال‌های اول هنوز خام بود و محدودیت‌های قابل توجهی داشت. حالا با iOS 26، داستان عوض شده. این فریمورک واقعاً به یه نقطه عطف رسیده و قابلیت‌هایی مثل وراثت مدل (Model Inheritance)، بهبود مهاجرت اسکیما، یکپارچگی عمیق‌تر با CloudKit و پشتیبانی بهتر از Swift Concurrency، اون رو به یه راه‌حل جامع برای لایه داده تبدیل کرده.

اگه با Core Data کار کرده باشید، احتمالاً درد و رنجش رو خوب می‌شناسید: فایل‌های .xcdatamodeld با اون ویرایشگر گرافیکی عجیب، زیرکلاس‌های NSManagedObject با @NSManaged، رفتار غیرقابل پیش‌بینی NSFetchRequest و پیچیدگی‌های NSPersistentContainer. من شخصاً ساعت‌ها وقتم رو صرف دیباگ کردن مشکلات عجیب Core Data کردم. SwiftData تمام این پیچیدگی‌ها رو حذف می‌کنه. به جای ویرایشگر گرافیکی، کلاس‌های Swift خودتون رو با ماکرو @Model تعریف می‌کنید. به جای NSFetchRequest، از #Predicate استفاده می‌کنید که type-safe هست و در زمان کامپایل بررسی می‌شه. به جای NSPersistentContainer، یه ModelContainer دارید که با یه خط کد قابل پیکربندیه.

تو این مقاله، SwiftData رو به صورت جامع بررسی می‌کنیم — از مفاهیم پایه و تعریف مدل‌ها، تا قابلیت جدید وراثت مدل در iOS 26، مهاجرت اسکیما با VersionedSchema، کوئری‌های پیشرفته، عملیات پس‌زمینه با @ModelActor و همگام‌سازی با CloudKit. هدفمون اینه که یه راهنمای عملی و کاربردی ارائه بدیم که بتونید مستقیماً تو پروژه‌هاتون ازش استفاده کنید.

مفاهیم پایه SwiftData

قبل از اینکه بریم سراغ جزئیات پیاده‌سازی، بیاید با چهار ستون اصلی معماری SwiftData آشنا بشیم.

هر کدوم از این اجزا نقش مشخصی دارن و در کنار هم یه سیستم کامل مدیریت داده رو تشکیل می‌دن.

ماکرو @Model — تعریف مدل‌های داده

ماکرو @Model نقطه ورود به دنیای SwiftData هست. وقتی این ماکرو رو روی یه کلاس اعمال می‌کنید، کامپایلر به صورت خودکار تمام کد لازم برای ذخیره‌سازی رو تولید می‌کنه. کلاس به طور خودکار با پروتکل PersistentModel سازگار می‌شه و تمام ویژگی‌ها (properties) برای تغییرات ردیابی می‌شن. نکته‌ای که اینجا مهمه اینه که SwiftData از ماکروهای Swift استفاده می‌کنه تا این کار رو در زمان کامپایل انجام بده، نه در زمان اجرا — و این تفاوت بزرگیه.

ModelContainer — کانتینر ذخیره‌سازی

ModelContainer مسئول پیکربندی کامل ذخیره‌گاه هست — محل ذخیره‌سازی داده‌ها، اسکیمای مورد استفاده، و اینکه ذخیره‌گاه in-memory باشه یا روی دیسک. این کلاس معادل NSPersistentContainer در Core Data هست ولی به مراتب ساده‌تر برای راه‌اندازی.

ModelContext — محیط عملیاتی

ModelContext محیطیه که تمام عملیات CRUD (ایجاد، خواندن، به‌روزرسانی و حذف) توش انجام می‌شه. این کلاس تغییرات رو ردیابی می‌کنه و در ذخیره‌گاه ثبتشون می‌کنه. در SwiftUI از طریق @Environment(\.modelContext) بهش دسترسی دارید.

ModelConfiguration — پیکربندی ذخیره‌سازی

ModelConfiguration بهتون امکان می‌ده جزئیات ذخیره‌سازی رو کنترل کنید: نام فایل، مسیر ذخیره‌سازی، فعال‌سازی CloudKit و تعیین حالت فقط‌خواندنی. جالبه که می‌تونید چندین پیکربندی مختلف رو تو یه اپلیکیشن داشته باشید.

خب بیاید ببینیم این اجزا در عمل چطور با هم کار می‌کنن:

import SwiftUI
import SwiftData

// تعریف یک مدل ساده
@Model
final class Trip {
    var destination: String
    var startDate: Date
    var endDate: Date
    var notes: String?

    init(destination: String, startDate: Date, endDate: Date, notes: String? = nil) {
        self.destination = destination
        self.startDate = startDate
        self.endDate = endDate
        self.notes = notes
    }
}

// پیکربندی و راه‌اندازی در اپلیکیشن
@main
struct TripPlannerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        // ایجاد کانتینر با مدل Trip
        .modelContainer(for: Trip.self)
    }
}

// استفاده در SwiftUI با @Query
struct ContentView: View {
    @Query(sort: \Trip.startDate, order: .reverse)
    private var trips: [Trip]

    @Environment(\.modelContext) private var context

    var body: some View {
        List(trips) { trip in
            VStack(alignment: .leading) {
                Text(trip.destination)
                    .font(.headline)
                Text(trip.startDate, style: .date)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

دیدید؟ با چند خط کد، یه سیستم کامل ذخیره‌سازی و بازیابی داده راه‌اندازی شد. ماکرو @Query در SwiftUI جایگزین @FetchRequest از Core Data هست و به صورت خودکار UI رو با تغییرات داده همگام نگه می‌داره. واقعاً وقتی مقایسه‌اش می‌کنی با حجم boilerplate ای که Core Data نیاز داشت، تفاوت چشمگیره.

وراثت مدل در iOS 26 — ستاره قابلیت‌های جدید

یکی از مهم‌ترین محدودیت‌های SwiftData از همون اول، نبود پشتیبانی از وراثت کلاس (Class Inheritance) بود. تو Core Data، وراثت موجودیت‌ها (Entity Inheritance) یه قابلیت اساسی بود که خیلی از توسعه‌دهنده‌ها بهش وابسته بودن. خب حالا با iOS 26، SwiftData بالاخره از وراثت مدل پشتیبانی می‌کنه و صادقانه بگم، این قابلیت بازی رو کاملاً عوض می‌کنه.

وراثت مدل بهتون اجازه می‌ده یه کلاس پایه با ماکرو @Model تعریف کنید و بعد زیرکلاس‌هایی ازش بسازید که ویژگی‌های اختصاصی خودشون رو اضافه می‌کنن. تمام زیرکلاس‌ها ویژگی‌های کلاس پایه رو به ارث می‌برن و در عین حال می‌تونن ویژگی‌های خاص خودشون رو هم داشته باشن.

تعریف کلاس پایه و زیرکلاس‌ها

import SwiftData

// کلاس پایه سفر
@Model
class Trip {
    var destination: String
    var startDate: Date
    var endDate: Date
    var notes: String?

    // مدت سفر به صورت محاسبه‌شده
    var duration: TimeInterval {
        endDate.timeIntervalSince(startDate)
    }

    init(destination: String, startDate: Date, endDate: Date, notes: String? = nil) {
        self.destination = destination
        self.startDate = startDate
        self.endDate = endDate
        self.notes = notes
    }
}

// سفر کاری — زیرکلاس با ویژگی‌های اضافی
@available(iOS 26, *)
@Model
final class BusinessTrip: Trip {
    var company: String
    var budget: Decimal
    var meetingAgenda: String?
    var expenseReport: Data?

    init(
        destination: String,
        startDate: Date,
        endDate: Date,
        company: String,
        budget: Decimal,
        meetingAgenda: String? = nil
    ) {
        self.company = company
        self.budget = budget
        self.meetingAgenda = meetingAgenda
        // فراخوانی سازنده کلاس پایه
        super.init(
            destination: destination,
            startDate: startDate,
            endDate: endDate
        )
    }
}

// سفر شخصی — زیرکلاس دیگر
@available(iOS 26, *)
@Model
final class PersonalTrip: Trip {
    var activities: [String]
    var companion: String?
    var isRelaxation: Bool

    init(
        destination: String,
        startDate: Date,
        endDate: Date,
        activities: [String] = [],
        companion: String? = nil,
        isRelaxation: Bool = true
    ) {
        self.activities = activities
        self.companion = companion
        self.isRelaxation = isRelaxation
        super.init(
            destination: destination,
            startDate: startDate,
            endDate: endDate
        )
    }
}

ثبت زیرکلاس‌ها در ModelContainer

یه نکته خیلی مهم که ممکنه خیلی‌ها اولش غافلگیرشون کنه: وقتی از وراثت مدل استفاده می‌کنید، باید تمام زیرکلاس‌ها رو در ModelContainer ثبت کنید. صرفاً ثبت کلاس پایه کافی نیست. SwiftData نیاز داره هر زیرکلاس رو بشناسه تا بتونه اون‌ها رو درست ذخیره و بازیابی کنه.

import SwiftUI
import SwiftData

@available(iOS 26, *)
@main
struct TripPlannerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        // ثبت تمام زیرکلاس‌ها — این مرحله حیاتی است
        .modelContainer(for: [
            Trip.self,
            BusinessTrip.self,
            PersonalTrip.self
        ])
    }
}

کوئری زدن روی سلسله‌مراتب وراثت

یکی از مزایای خیلی خوب وراثت مدل اینه که می‌تونید روی کلاس پایه کوئری بزنید و تمام زیرکلاس‌ها رو دریافت کنید، یا مستقیم روی یه زیرکلاس خاص کوئری بزنید. این انعطاف‌پذیری واقعاً قدرتمنده:

@available(iOS 26, *)
struct TripListView: View {
    // دریافت تمام سفرها — شامل کاری و شخصی
    @Query(sort: \Trip.startDate)
    private var allTrips: [Trip]

    // فقط سفرهای کاری
    @Query(sort: \BusinessTrip.startDate)
    private var businessTrips: [BusinessTrip]

    // فقط سفرهای شخصی با فیلتر
    @Query(filter: #Predicate { $0.isRelaxation == true })
    private var relaxationTrips: [PersonalTrip]

    var body: some View {
        NavigationStack {
            List {
                Section("تمام سفرها") {
                    ForEach(allTrips) { trip in
                        TripRow(trip: trip)
                    }
                }

                Section("سفرهای کاری") {
                    ForEach(businessTrips) { trip in
                        BusinessTripRow(trip: trip)
                    }
                }
            }
            .navigationTitle("برنامه‌ریز سفر")
        }
    }
}

یادتون باشه که @available(iOS 26, *) باید برای هر کدی که از وراثت مدل استفاده می‌کنه اعمال بشه. این قابلیت فقط در iOS 26 و بالاتر در دسترسه. اگه نیاز به پشتیبانی از نسخه‌های قدیمی‌تر دارید، باید از الگوی Composition به جای Inheritance استفاده کنید — که خب، کمی دردناک‌تره ولی راه‌حل عملیه.

تعریف روابط بین مدل‌ها

روابط (Relationships) در SwiftData با ماکرو @Relationship تعریف می‌شن. این ماکرو بهتون امکان می‌ده نوع رابطه و قوانین حذف رو مشخص کنید. SwiftData از روابط یک‌به‌یک، یک‌به‌چند و چندبه‌چند پشتیبانی می‌کنه.

رابطه یک‌به‌چند با قانون حذف آبشاری

import SwiftData

@Model
final class Traveler {
    var name: String
    var email: String
    var passportNumber: String?

    // رابطه یک‌به‌چند: یک مسافر چندین رزرو دارد
    // حذف آبشاری: با حذف مسافر، رزروها هم حذف می‌شوند
    @Relationship(deleteRule: .cascade, inverse: \Booking.traveler)
    var bookings: [Booking]

    // رابطه یک‌به‌چند با سفرها
    @Relationship(deleteRule: .nullify, inverse: \Trip.organizer)
    var organizedTrips: [Trip]

    init(name: String, email: String, passportNumber: String? = nil) {
        self.name = name
        self.email = email
        self.passportNumber = passportNumber
        self.bookings = []
        self.organizedTrips = []
    }
}

@Model
final class Booking {
    var confirmationCode: String
    var bookingDate: Date
    var totalPrice: Decimal
    var status: BookingStatus

    // طرف معکوس رابطه
    var traveler: Traveler?

    init(confirmationCode: String, bookingDate: Date, totalPrice: Decimal, status: BookingStatus = .confirmed) {
        self.confirmationCode = confirmationCode
        self.bookingDate = bookingDate
        self.totalPrice = totalPrice
        self.status = status
    }
}

enum BookingStatus: String, Codable {
    case pending
    case confirmed
    case cancelled
    case completed
}

رابطه چندبه‌چند

روابط چندبه‌چند تو SwiftData خیلی ساده پیاده‌سازی می‌شن. کافیه آرایه‌ها رو در هر دو طرف رابطه تعریف کنید و SwiftData خودش جدول واسط رو مدیریت می‌کنه (دیگه لازم نیست دستی جدول واسط بسازید):

@Model
final class Tag {
    var name: String
    var color: String

    // رابطه چندبه‌چند: هر تگ می‌تواند روی چندین سفر باشد
    @Relationship(inverse: \Trip.tags)
    var trips: [Trip]

    init(name: String, color: String) {
        self.name = name
        self.color = color
        self.trips = []
    }
}

// اضافه کردن تگ‌ها به مدل Trip
@Model
class Trip {
    var destination: String
    var startDate: Date
    var endDate: Date
    var organizer: Traveler?

    // رابطه چندبه‌چند با تگ‌ها
    var tags: [Tag]

    init(destination: String, startDate: Date, endDate: Date) {
        self.destination = destination
        self.startDate = startDate
        self.endDate = endDate
        self.tags = []
    }
}

قوانین حذف در SwiftData سه گزینه اصلی دارن:

  • .cascade — حذف آبشاری: با حذف شیء والد، تمام اشیاء فرزند هم حذف می‌شن. مناسب برای روابطی که فرزند بدون والد معنی نداره.
  • .nullify — خنثی‌سازی: با حذف شیء والد، ارجاع در فرزندان به nil تبدیل می‌شه. این گزینه پیش‌فرض SwiftData هست.
  • .deny — جلوگیری: اجازه حذف والد داده نمی‌شه تا زمانی که فرزندان وجود دارن. توجه کنید که این قانون با CloudKit سازگار نیست.

مهاجرت اسکیما با VersionedSchema

یکی از چالش‌های همیشگی تو هر سیستم ذخیره‌سازی، مدیریت تغییرات اسکیما در طول زمانه. فکرش رو بکنید — نسخه جدید اپلیکیشن رو منتشر می‌کنید، ساختار مدل‌ها تغییر کرده، و باید داده‌های قبلی کاربران به اسکیمای جدید منتقل بشن. SwiftData این کار رو با دو ابزار اصلی انجام می‌ده: پروتکل VersionedSchema و پروتکل SchemaMigrationPlan.

تعریف نسخه‌های اسکیما

هر نسخه از اسکیما تو یه enum مجزا تعریف می‌شه که پروتکل VersionedSchema رو پیاده‌سازی می‌کنه. هر نسخه باید شامل تعریف کامل مدل‌ها در اون نسخه باشه:

import SwiftData

// نسخه اول اسکیما
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Trip.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
        }
    }
}

// نسخه دوم اسکیما — اضافه شدن فیلدهای جدید
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Trip.self]
    }

    @Model
    class Trip {
        var destination: String
        var startDate: Date
        var endDate: Date
        // فیلدهای جدید در نسخه ۲
        var notes: String?
        var priority: Int
        var isFavorite: Bool

        init(
            destination: String,
            startDate: Date,
            endDate: Date,
            notes: String? = nil,
            priority: Int = 0,
            isFavorite: Bool = false
        ) {
            self.destination = destination
            self.startDate = startDate
            self.endDate = endDate
            self.notes = notes
            self.priority = priority
            self.isFavorite = isFavorite
        }
    }
}

// نسخه سوم — تغییر ساختاری بزرگ‌تر
enum SchemaV3: VersionedSchema {
    static var versionIdentifier = Schema.Version(3, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Trip.self, Destination.self]
    }

    @Model
    class Trip {
        var startDate: Date
        var endDate: Date
        var notes: String?
        var priority: Int
        var isFavorite: Bool
        // تغییر: destination از String به یک مدل مجزا تبدیل شده
        @Relationship(deleteRule: .nullify)
        var destination: Destination?

        init(startDate: Date, endDate: Date) {
            self.startDate = startDate
            self.endDate = endDate
            self.priority = 0
            self.isFavorite = false
        }
    }

    @Model
    class Destination {
        var name: String
        var country: String
        var latitude: Double?
        var longitude: Double?

        init(name: String, country: String) {
            self.name = name
            self.country = country
        }
    }
}

تعریف طرح مهاجرت (Migration Plan)

بعد از تعریف نسخه‌های اسکیما، باید یه SchemaMigrationPlan بنویسید که مشخص کنه چطور از هر نسخه به نسخه بعدی مهاجرت انجام بشه. دو نوع مهاجرت وجود داره:

  • مهاجرت سبک (Lightweight) — برای تغییرات ساده مثل اضافه کردن فیلدهای اختیاری یا فیلدهای با مقدار پیش‌فرض. SwiftData خودش مهاجرت رو انجام می‌ده.
  • مهاجرت سفارشی (Custom) — برای تغییرات پیچیده مثل تقسیم یا ادغام فیلدها، تبدیل نوع داده یا انتقال داده بین مدل‌ها. اینجا باید خودتون منطق مهاجرت رو بنویسید.
enum TripMigrationPlan: SchemaMigrationPlan {
    // لیست تمام نسخه‌ها به ترتیب
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }

    // مراحل مهاجرت بین نسخه‌ها
    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    // مهاجرت سبک: از نسخه ۱ به ۲
    // فقط فیلدهای اختیاری یا با مقدار پیش‌فرض اضافه شده‌اند
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )

    // مهاجرت سفارشی: از نسخه ۲ به ۳
    // نیاز به تبدیل رشته destination به مدل Destination
    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SchemaV2.self,
        toVersion: SchemaV3.self,
        willMigrate: { context in
            // دریافت تمام سفرهای نسخه قبلی
            let oldTrips = try context.fetch(
                FetchDescriptor()
            )

            // ایجاد مدل‌های Destination از رشته‌های destination
            var destinationCache: [String: SchemaV3.Destination] = [:]

            for oldTrip in oldTrips {
                let destName = oldTrip.destination
                if destinationCache[destName] == nil {
                    let newDest = SchemaV3.Destination(
                        name: destName,
                        country: "نامشخص"
                    )
                    context.insert(newDest)
                    destinationCache[destName] = newDest
                }
            }

            try context.save()
        },
        didMigrate: { context in
            // عملیات پس از مهاجرت — مثلاً پاکسازی داده‌های قدیمی
            print("مهاجرت از نسخه ۲ به ۳ با موفقیت انجام شد")
            try context.save()
        }
    )
}

اعمال طرح مهاجرت در اپلیکیشن

برای فعال‌سازی مهاجرت، باید ModelContainer رو با ModelConfiguration و طرح مهاجرت پیکربندی کنید:

@main
struct TripPlannerApp: App {
    let container: ModelContainer

    init() {
        do {
            let configuration = ModelConfiguration(
                schema: Schema(versionedSchema: SchemaV3.self),
                // فعال‌سازی CloudKit در صورت نیاز
                cloudKitDatabase: .automatic
            )

            container = try ModelContainer(
                for: SchemaV3.Trip.self, SchemaV3.Destination.self,
                migrationPlan: TripMigrationPlan.self,
                configurations: [configuration]
            )
        } catch {
            fatalError("خطا در ایجاد ModelContainer: \(error)")
        }
    }

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

یه نکته مهم: حتماً طرح مهاجرت رو قبل از انتشار اپلیکیشن به صورت کامل تست کنید. جدی می‌گم — یه مهاجرت ناموفق می‌تونه به از دست رفتن داده‌های کاربران منجر بشه و هیچ چیزی بدتر از این نیست. بهترین روش، نوشتن تست‌های واحد برای هر مرحله مهاجرت با استفاده از کانتینرهای in-memory هست.

کوئری‌های پیشرفته با FetchDescriptor و Predicate

SwiftData یه سیستم کوئری قدرتمند و type-safe ارائه می‌ده. ماکرو #Predicate بهتون اجازه می‌ده شرایط فیلتر رو با سینتکس طبیعی Swift بنویسید و کامپایلر اون‌ها رو در زمان کامپایل بررسی می‌کنه. دیگه خبری از کابوس NSPredicate با رشته‌های فرمت‌شده نیست!

FetchDescriptor و #Predicate

FetchDescriptor معادل NSFetchRequest در Core Data هست ولی خیلی ساده‌تر و قدرتمندتره. با ترکیب #Predicate، SortDescriptor و تنظیمات صفحه‌بندی، می‌تونید کوئری‌های پیچیده بسازید:

import SwiftData

struct TripQueryService {
    let context: ModelContext

    // جستجوی ساده با فیلتر
    func fetchUpcomingTrips() throws -> [Trip] {
        let now = Date()
        let predicate = #Predicate { trip in
            trip.startDate > now
        }

        let descriptor = FetchDescriptor(
            predicate: predicate,
            sortBy: [SortDescriptor(\.startDate, order: .forward)]
        )

        return try context.fetch(descriptor)
    }

    // جستجوی پیشرفته با چندین شرط
    func searchTrips(
        keyword: String,
        minDuration: TimeInterval,
        favoritesOnly: Bool
    ) throws -> [Trip] {
        let predicate = #Predicate { trip in
            (trip.destination.localizedStandardContains(keyword) ||
             (trip.notes?.localizedStandardContains(keyword) ?? false)) &&
            trip.isFavorite == favoritesOnly
        }

        var descriptor = FetchDescriptor(predicate: predicate)
        // مرتب‌سازی بر اساس اولویت و تاریخ
        descriptor.sortBy = [
            SortDescriptor(\.priority, order: .reverse),
            SortDescriptor(\.startDate, order: .forward)
        ]

        return try context.fetch(descriptor)
    }

    // صفحه‌بندی (Pagination)
    func fetchTrips(page: Int, pageSize: Int) throws -> [Trip] {
        var descriptor = FetchDescriptor(
            sortBy: [SortDescriptor(\.startDate, order: .reverse)]
        )
        descriptor.fetchLimit = pageSize
        descriptor.fetchOffset = page * pageSize

        return try context.fetch(descriptor)
    }

    // شمارش بدون بارگذاری تمام داده‌ها
    func countFavoriteTrips() throws -> Int {
        let predicate = #Predicate { $0.isFavorite == true }
        let descriptor = FetchDescriptor(predicate: predicate)
        return try context.fetchCount(descriptor)
    }

    // دریافت شناسه‌ها بدون بارگذاری اشیاء کامل
    func fetchTripIdentifiers() throws -> [PersistentIdentifier] {
        let descriptor = FetchDescriptor(
            sortBy: [SortDescriptor(\.startDate)]
        )
        return try context.fetchIdentifiers(descriptor)
    }
}

یه نکته‌ای که خوبه از همون اول بدونید: #Predicate از تمام عملگرهای Swift پشتیبانی نمی‌کنه. عملیاتی که در سطح SQLite قابل ترجمه نیستن (مثل فراخوانی متدهای سفارشی روی اشیاء) تو #Predicate مجاز نیستن. همیشه از عملگرهای ساده مقایسه‌ای، عملگرهای منطقی و متدهای رشته‌ای مثل contains و localizedStandardContains استفاده کنید.

استفاده از @Query با پارامترهای پویا

تو SwiftUI، ماکرو @Query راحت‌ترین روش دسترسی به داده‌هاست. ولی گاهی نیاز دارید فیلترها رو به صورت پویا تغییر بدید. برای این کار می‌تونید @Query رو در init پیکربندی کنید:

struct FilteredTripList: View {
    @Query private var trips: [Trip]

    init(showFavoritesOnly: Bool, sortOrder: SortOrder) {
        let predicate: Predicate? = showFavoritesOnly
            ? #Predicate { $0.isFavorite == true }
            : nil

        _trips = Query(
            filter: predicate,
            sort: \.startDate,
            order: sortOrder
        )
    }

    var body: some View {
        List(trips) { trip in
            TripRow(trip: trip)
        }
    }
}

استفاده از @ModelActor برای عملیات پس‌زمینه

یکی از نقاط قوت SwiftData یکپارچگی عمیقش با Swift Concurrency هست. ماکرو @ModelActor بهتون اجازه می‌ده یه Actor تعریف کنید که ModelContext اختصاصی خودش رو داره و می‌تونه عملیات سنگین رو تو پس‌زمینه انجام بده بدون اینکه رابط کاربری قفل بشه.

چرا این مهمه؟ تو اپلیکیشن‌های واقعی، عملیاتی مثل وارد کردن حجم زیادی از داده‌ها، همگام‌سازی با سرور یا گزارش‌گیری پیچیده نباید روی ترد اصلی انجام بشن. اگه این کار رو بکنید، UI کاملاً فریز می‌شه و تجربه کاربری افتضاح می‌شه. @ModelActor دقیقاً این مشکل رو حل می‌کنه.

import SwiftData

// تعریف ساختار داده ورودی
struct TripData: Sendable {
    let destination: String
    let startDate: Date
    let endDate: Date
    let notes: String?
    let isFavorite: Bool
}

// مدیر داده با @ModelActor
@ModelActor
actor DataManager {

    // وارد کردن دسته‌ای سفرها
    func importTrips(_ data: [TripData]) throws -> Int {
        var importedCount = 0

        for item in data {
            let trip = Trip(
                destination: item.destination,
                startDate: item.startDate,
                endDate: item.endDate,
                notes: item.notes
            )
            trip.isFavorite = item.isFavorite
            modelContext.insert(trip)
            importedCount += 1
        }

        // ذخیره تمام تغییرات یکجا — بهینه‌تر از ذخیره تک‌تک
        try modelContext.save()
        return importedCount
    }

    // حذف سفرهای قدیمی
    func deleteOldTrips(before date: Date) throws -> Int {
        let predicate = #Predicate { trip in
            trip.endDate < date
        }
        let descriptor = FetchDescriptor(predicate: predicate)
        let oldTrips = try modelContext.fetch(descriptor)
        let count = oldTrips.count

        for trip in oldTrips {
            modelContext.delete(trip)
        }

        try modelContext.save()
        return count
    }

    // به‌روزرسانی دسته‌ای
    func markAllAsFavorite(destination: String) throws {
        let predicate = #Predicate { trip in
            trip.destination == destination
        }
        let descriptor = FetchDescriptor(predicate: predicate)
        let trips = try modelContext.fetch(descriptor)

        for trip in trips {
            trip.isFavorite = true
        }

        try modelContext.save()
    }
}

// استفاده در SwiftUI
struct ImportView: View {
    @Environment(\.modelContext) private var context
    @State private var isImporting = false
    @State private var importResult: String?

    var body: some View {
        VStack {
            Button("وارد کردن داده‌ها") {
                isImporting = true
                Task {
                    await performImport()
                    isImporting = false
                }
            }
            .disabled(isImporting)

            if isImporting {
                ProgressView("در حال وارد کردن...")
            }

            if let result = importResult {
                Text(result)
                    .foregroundStyle(.green)
            }
        }
    }

    private func performImport() async {
        // ایجاد DataManager با ModelContainer
        let container = context.container
        let manager = DataManager(modelContainer: container)

        do {
            let sampleData = generateSampleData()
            let count = try await manager.importTrips(sampleData)
            importResult = "\(count) سفر با موفقیت وارد شد"
        } catch {
            importResult = "خطا: \(error.localizedDescription)"
        }
    }

    private func generateSampleData() -> [TripData] {
        // تولید داده نمونه
        return [
            TripData(
                destination: "استانبول",
                startDate: Date(),
                endDate: Date().addingTimeInterval(86400 * 5),
                notes: "سفر تابستانی",
                isFavorite: true
            ),
            TripData(
                destination: "دبی",
                startDate: Date().addingTimeInterval(86400 * 30),
                endDate: Date().addingTimeInterval(86400 * 37),
                notes: nil,
                isFavorite: false
            )
        ]
    }
}

یه نکته مهم درباره @ModelActor که باید حتماً بدونید: هر Actor یه ModelContext مجزا و اختصاصی داره. یعنی تغییراتی که تو Actor انجام می‌دید، بلافاصله تو ModelContext اصلی (که تو UI استفاده می‌شه) منعکس نمی‌شن. SwiftData از طریق مکانیزم merge این تغییرات رو همگام می‌کنه، ولی ممکنه یه تاخیر کوچک وجود داشته باشه. همچنین نمی‌تونید اشیاء PersistentModel رو مستقیماً بین Actorها منتقل کنید — باید از PersistentIdentifier استفاده کنید.

همگام‌سازی با CloudKit

یکی از قابلیت‌های جذاب SwiftData، یکپارچگی بومی با CloudKit هست. با فقط چند خط پیکربندی، می‌تونید داده‌های اپلیکیشن رو بین تمام دستگاه‌های کاربر همگام‌سازی کنید. ولی خب، این یکپارچگی بدون محدودیت نیست و باید از همون اول این محدودیت‌ها رو تو طراحی اسکیما در نظر بگیرید.

فعال‌سازی CloudKit

برای فعال‌سازی همگام‌سازی CloudKit، سه مرحله دارید:

  1. قابلیت CloudKit رو در Signing & Capabilities پروژه فعال کنید.
  2. یه CloudKit Container تو Developer Portal ایجاد کنید.
  3. ModelConfiguration رو با cloudKitDatabase پیکربندی کنید.
import SwiftData

@main
struct TripPlannerApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Trip.self) { result in
            switch result {
            case .success(let container):
                print("کانتینر CloudKit آماده است")
            case .failure(let error):
                print("خطا در راه‌اندازی: \(error)")
            }
        }
    }
}

// یا با پیکربندی دستی
func createCloudKitContainer() throws -> ModelContainer {
    let config = ModelConfiguration(
        "TripStore",
        schema: Schema([Trip.self]),
        // فعال‌سازی CloudKit
        cloudKitDatabase: .automatic
    )

    return try ModelContainer(
        for: Trip.self,
        configurations: [config]
    )
}

محدودیت‌های اسکیما برای CloudKit

CloudKit یه سری محدودیت‌ها روی ساختار داده‌ها اعمال می‌کنه که باید از همون اول رعایت بشن. اگه رعایت نکنید، با خطاهای زمان اجرا مواجه می‌شید:

  • تمام ویژگی‌ها باید اختیاری باشن یا مقدار پیش‌فرض داشته باشن — CloudKit نمی‌تونه تضمین کنه که تمام فیلدها هنگام همگام‌سازی مقدار دارن.
  • روابط باید اختیاری باشن — نمی‌تونید روابط غیراختیاری داشته باشید چون ممکنه شیء مرتبط هنوز همگام نشده باشه.
  • محدودیت‌های یکتایی (.unique) پشتیبانی نمی‌شن — CloudKit اصلاً از محدودیت‌های یکتایی پشتیبانی نمی‌کنه.
  • قانون حذف .deny پشتیبانی نمی‌شه — فقط .cascade و .nullify مجازن.

بیاید یه مدل سازگار با CloudKit ببینیم:

import SwiftData

// مدل سازگار با CloudKit
@Model
final class CloudTrip {
    // تمام فیلدها یا اختیاری هستند یا مقدار پیش‌فرض دارند
    var destination: String = ""
    var startDate: Date = Date()
    var endDate: Date = Date()
    var notes: String?
    var priority: Int = 0
    var isFavorite: Bool = false

    // رابطه اختیاری — الزامی برای CloudKit
    @Relationship(deleteRule: .cascade)
    var bookings: [CloudBooking]?

    // رابطه معکوس اختیاری
    var organizer: CloudTraveler?

    init() {
        self.bookings = []
    }
}

@Model
final class CloudBooking {
    var confirmationCode: String = ""
    var bookingDate: Date = Date()
    var totalPrice: Decimal = 0

    // رابطه معکوس اختیاری
    var trip: CloudTrip?

    init() {}
}

@Model
final class CloudTraveler {
    var name: String = ""
    var email: String = ""

    @Relationship(deleteRule: .nullify)
    var trips: [CloudTrip]?

    init() {
        self.trips = []
    }
}

اصل طلایی CloudKit: فقط اضافه کن، حذف نکن، تغییر نده

این رو خوب تو ذهنتون نگه دارید: وقتی اسکیمای CloudKit یه بار روی سرورهای اپل مستقر (Deploy) شد، دیگه نمی‌تونید تغییرش بدید. این محدودیت شاید اول آزاردهنده به نظر برسه ولی دلیل منطقی داره.

  • می‌تونید فیلدهای جدید اضافه کنید.
  • نمی‌تونید فیلدهای موجود رو حذف کنید.
  • نمی‌تونید نوع فیلدهای موجود رو تغییر بدید.
  • نمی‌تونید نام فیلدها یا رکوردها رو عوض کنید.

دلیلش اینه که ممکنه کاربرانی با نسخه‌های قدیمی اپلیکیشن هنوز از اسکیمای قبلی استفاده کنن. اگه اسکیما رو تغییر بدید، همگام‌سازی برای اون‌ها از کار می‌افته. به همین خاطر، طراحی اولیه اسکیمای CloudKit خیلی خیلی مهمه و باید با دقت انجام بشه.

توصیه می‌کنم همیشه اول اسکیما رو تو محیط Development تست کنید و فقط بعد از اطمینان کامل، به محیط Production منتقلش کنید. برای مدیریت نسخه‌های مختلف اسکیما تو CloudKit هم به جای تغییر فیلدها، فیلدهای جدید اضافه کنید و تو کد اپلیکیشن، منطق مهاجرت محلی رو پیاده‌سازی کنید.

الگوهای معماری و بهترین شیوه‌ها

استفاده درست از SwiftData فقط به دونستن APIها محدود نمی‌شه. طراحی معماری مناسب و رعایت بهترین شیوه‌ها تفاوت بین یه اپلیکیشن پایدار و یه اپلیکیشن شکننده‌ست. بیاید تو این بخش الگوهای معماری کلیدی رو بررسی کنیم.

لایه دسترسی داده (Data Access Layer)

از نظر من، یکی از مهم‌ترین الگوهای معماری، جداسازی منطق دسترسی به داده از لایه UI هست. با ایجاد یه لایه انتزاعی، کدتون تست‌پذیرتر و نگهداری‌پذیرتر می‌شه:

import SwiftData

// پروتکل لایه دسترسی داده
protocol TripRepositoryProtocol: Sendable {
    func fetchAll() async throws -> [Trip]
    func fetchUpcoming() async throws -> [Trip]
    func create(_ data: TripData) async throws -> Trip
    func delete(_ trip: Trip) async throws
    func search(keyword: String) async throws -> [Trip]
}

// پیاده‌سازی واقعی با SwiftData
@ModelActor
actor TripRepository: TripRepositoryProtocol {

    func fetchAll() throws -> [Trip] {
        let descriptor = FetchDescriptor(
            sortBy: [SortDescriptor(\.startDate, order: .reverse)]
        )
        return try modelContext.fetch(descriptor)
    }

    func fetchUpcoming() throws -> [Trip] {
        let now = Date()
        let predicate = #Predicate { $0.startDate > now }
        let descriptor = FetchDescriptor(
            predicate: predicate,
            sortBy: [SortDescriptor(\.startDate)]
        )
        return try modelContext.fetch(descriptor)
    }

    func create(_ data: TripData) throws -> Trip {
        let trip = Trip(
            destination: data.destination,
            startDate: data.startDate,
            endDate: data.endDate,
            notes: data.notes
        )
        modelContext.insert(trip)
        try modelContext.save()
        return trip
    }

    func delete(_ trip: Trip) throws {
        modelContext.delete(trip)
        try modelContext.save()
    }

    func search(keyword: String) throws -> [Trip] {
        let predicate = #Predicate { trip in
            trip.destination.localizedStandardContains(keyword)
        }
        let descriptor = FetchDescriptor(predicate: predicate)
        return try modelContext.fetch(descriptor)
    }
}

تست با کانتینرهای in-memory

یکی از بزرگ‌ترین مزایای SwiftData سهولت نوشتن تسته. با استفاده از کانتینرهای in-memory، تست‌هایی می‌نویسید که سریع اجرا می‌شن و هیچ اثر جانبی ندارن. این واقعاً نسبت به Core Data پیشرفت بزرگیه:

import Testing
import SwiftData

@Suite("تست‌های مخزن سفر")
struct TripRepositoryTests {

    // ساخت کانتینر تست — فقط در حافظه
    private func makeTestContainer() throws -> ModelContainer {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        return try ModelContainer(
            for: Trip.self, Booking.self, Tag.self,
            configurations: [config]
        )
    }

    @Test("ایجاد سفر جدید و بازیابی آن")
    func createAndFetchTrip() async throws {
        let container = try makeTestContainer()
        let context = ModelContext(container)

        // ایجاد سفر
        let trip = Trip(
            destination: "تهران",
            startDate: Date(),
            endDate: Date().addingTimeInterval(86400 * 3)
        )
        context.insert(trip)
        try context.save()

        // بازیابی و بررسی
        let descriptor = FetchDescriptor()
        let trips = try context.fetch(descriptor)

        #expect(trips.count == 1)
        #expect(trips.first?.destination == "تهران")
    }

    @Test("حذف آبشاری رزروها با حذف سفر")
    func cascadeDeleteBookings() async throws {
        let container = try makeTestContainer()
        let context = ModelContext(container)

        // ایجاد سفر با رزرو
        let trip = Trip(
            destination: "شیراز",
            startDate: Date(),
            endDate: Date().addingTimeInterval(86400)
        )
        context.insert(trip)

        let booking = Booking(
            confirmationCode: "ABC123",
            bookingDate: Date(),
            totalPrice: 500
        )
        booking.traveler = nil
        context.insert(booking)
        try context.save()

        // حذف سفر
        context.delete(trip)
        try context.save()

        // بررسی حذف آبشاری
        let remainingBookings = try context.fetch(FetchDescriptor())
        #expect(remainingBookings.isEmpty)
    }

    @Test("جستجوی سفر بر اساس مقصد")
    func searchByDestination() async throws {
        let container = try makeTestContainer()
        let context = ModelContext(container)

        // ایجاد چندین سفر
        let destinations = ["اصفهان", "استانبول", "اصفهان"]
        for dest in destinations {
            let trip = Trip(
                destination: dest,
                startDate: Date(),
                endDate: Date().addingTimeInterval(86400)
            )
            context.insert(trip)
        }
        try context.save()

        // جستجو
        let keyword = "اصفهان"
        let predicate = #Predicate { trip in
            trip.destination.localizedStandardContains(keyword)
        }
        let descriptor = FetchDescriptor(predicate: predicate)
        let results = try context.fetch(descriptor)

        #expect(results.count == 2)
    }
}

اکستنشن‌های کاربردی برای ModelContext

با ایجاد اکستنشن‌هایی روی ModelContext، عملیات رایج رو ساده‌تر و خواناتر کنید:

extension ModelContext {

    // دریافت یک شیء با شناسه
    func fetchOne(
        _ type: T.Type,
        identifier: PersistentIdentifier
    ) -> T? {
        return self.registeredModel(for: identifier) as T? ??
            (try? self.fetch(
                FetchDescriptor(
                    predicate: #Predicate { _ in true }
                )
            ))?.first(where: { $0.persistentModelID == identifier })
    }

    // ذخیره ایمن با مدیریت خطا
    func safeSave() throws {
        guard hasChanges else { return }
        try save()
    }

    // حذف تمام اشیاء از یک نوع
    func deleteAll(_ type: T.Type) throws {
        let descriptor = FetchDescriptor()
        let items = try fetch(descriptor)
        for item in items {
            delete(item)
        }
        try safeSave()
    }
}

الگوی ViewModel با SwiftData

در معماری MVVM، می‌تونید ViewModel رو با @Observable تعریف کنید و از ModelContext برای عملیات داده استفاده کنید. با این رویکرد، منطق تجاری از لایه View کاملاً جدا می‌شه:

import SwiftData
import Observation

@Observable
final class TripListViewModel {
    private let context: ModelContext

    var trips: [Trip] = []
    var searchText: String = ""
    var errorMessage: String?
    var isLoading: Bool = false

    init(context: ModelContext) {
        self.context = context
    }

    func loadTrips() {
        isLoading = true
        defer { isLoading = false }

        do {
            var descriptor = FetchDescriptor(
                sortBy: [SortDescriptor(\.startDate, order: .reverse)]
            )

            if !searchText.isEmpty {
                let keyword = searchText
                descriptor.predicate = #Predicate { trip in
                    trip.destination.localizedStandardContains(keyword)
                }
            }

            trips = try context.fetch(descriptor)
            errorMessage = nil
        } catch {
            errorMessage = "خطا در بارگذاری سفرها: \(error.localizedDescription)"
        }
    }

    func addTrip(destination: String, startDate: Date, endDate: Date) {
        let trip = Trip(
            destination: destination,
            startDate: startDate,
            endDate: endDate
        )
        context.insert(trip)

        do {
            try context.safeSave()
            loadTrips()
        } catch {
            errorMessage = "خطا در ذخیره سفر: \(error.localizedDescription)"
        }
    }

    func deleteTrip(_ trip: Trip) {
        context.delete(trip)
        do {
            try context.safeSave()
            loadTrips()
        } catch {
            errorMessage = "خطا در حذف سفر: \(error.localizedDescription)"
        }
    }
}

نکات عملکردی (Performance Tips)

برای بهینه‌سازی عملکرد SwiftData تو اپلیکیشن‌های واقعی، این نکات رو رعایت کنید:

  • از fetchLimit و fetchOffset استفاده کنید — به جای بارگذاری تمام داده‌ها، فقط مقدار مورد نیاز رو بگیرید. این به خصوص برای لیست‌های بلند حیاتیه.
  • از fetchCount برای شمارش استفاده کنید — اگه فقط به تعداد نیاز دارید، fetchCount خیلی سریع‌تر از دریافت تمام اشیاء و شمارش اون‌هاست.
  • از fetchIdentifiers استفاده کنید — وقتی فقط به شناسه‌ها نیاز دارید، این متد حافظه خیلی کمتری مصرف می‌کنه.
  • عملیات دسته‌ای رو تو @ModelActor انجام بدید — عملیات سنگین رو از ترد اصلی خارج کنید تا UI همیشه پاسخگو باشه.
  • از @Attribute(.externalStorage) برای داده‌های بزرگ استفاده کنید — فایل‌های تصویری و باینری بزرگ رو با این ویژگی خارج از دیتابیس اصلی ذخیره کنید.
  • autosaveEnabled رو در نظر بگیرید — تو ModelContext، ذخیره خودکار به صورت پیش‌فرض فعاله. اگه عملیات دسته‌ای انجام می‌دید، شاید بخواید غیرفعالش کنید و خودتون زمان ذخیره رو کنترل کنید.

نتیجه‌گیری

SwiftData تو iOS 26 واقعاً به یه فریمورک بالغ و کامل تبدیل شده. با اضافه شدن وراثت مدل، یکی از بزرگ‌ترین محدودیت‌های این فریمورک برطرف شده و حالا می‌تونید سلسله‌مراتب‌های پیچیده داده رو به صورت طبیعی و با استفاده از وراثت کلاس مدل‌سازی کنید.

تو این مقاله، تمام جنبه‌های کلیدی SwiftData رو بررسی کردیم:

  • مفاهیم پایه — ماکرو @Model، ModelContainer، ModelContext و ModelConfiguration به عنوان ستون‌های اصلی.
  • وراثت مدل — قابلیت جدید iOS 26 که امکان تعریف کلاس پایه و زیرکلاس‌ها با @Model رو فراهم می‌کنه.
  • روابط — تعریف روابط یک‌به‌چند و چندبه‌چند با ماکرو @Relationship و قوانین حذف مختلف.
  • مهاجرت اسکیما — مدیریت تغییرات ساختاری با VersionedSchema و SchemaMigrationPlan.
  • کوئری‌های پیشرفته — استفاده از FetchDescriptor، #Predicate و صفحه‌بندی.
  • عملیات پس‌زمینه — استفاده از @ModelActor برای عملیات سنگین با یکپارچگی کامل با Swift Concurrency.
  • CloudKit — همگام‌سازی داده‌ها بین دستگاه‌ها با رعایت محدودیت‌های اسکیما.
  • الگوهای معماری — لایه دسترسی داده، تست با کانتینرهای in-memory و اکستنشن‌های کاربردی.

اگه هنوز از Core Data استفاده می‌کنید، به نظرم iOS 26 بهترین زمان برای مهاجرت به SwiftData هست. فریمورک به اندازه کافی بالغ شده، ابزارهای مهاجرت قوی هستن و قابلیت‌های جدید مثل وراثت مدل، نیاز به خیلی از workaroundهایی که قبلاً استفاده می‌شد رو از بین می‌برن.

با SwiftData، لایه داده اپلیکیشنتون ساده‌تر، امن‌تر و نگهداری‌پذیرتر خواهد بود.

پیشنهاد می‌کنم با یه پروژه کوچک شروع کنید، مفاهیم پایه رو تمرین کنید و بعد به تدریج قابلیت‌های پیشرفته‌تر مثل وراثت، مهاجرت و CloudKit رو اضافه کنید. مستندات رسمی اپل و جلسات WWDC 2025 منابع خوبی برای یادگیری عمیق‌تر هستن.

درباره نویسنده Editorial Team

Our team of expert writers and editors.