دليل SwiftData الشامل: من الأساسيات إلى وراثة النماذج في iOS 26

دليل عملي شامل لإطار SwiftData — من تعريف النماذج والاستعلامات الآمنة نوعيًا إلى مزامنة CloudKit وترحيل المخطط ووراثة النماذج الجديدة في iOS 26، مع أمثلة كود جاهزة وأفضل الممارسات.

مقدمة: لماذا SwiftData هو مستقبل إدارة البيانات في تطبيقات Apple؟

إذا كنت مطوّر iOS — سواء محترف أو لسه في بداية الطريق — فالأكيد إنك تعاملت مع Core Data في مرحلة ما. الإطار العريق اللي رافق مطوّري Apple لأكثر من عقدين. صراحةً، Core Data كان ولا يزال أداة قوية، لكن دعنا نعترف: التعقيدات كانت كثيرة. NSManagedObjectContext، NSFetchRequest، NSPredicate بسلاسل نصية غير آمنة... والقائمة تطول.

في WWDC 2023، قدّمت Apple الحل اللي كثير من المطوّرين كانوا ينتظرونه: SwiftData.

إطار عمل حديث مبني بالكامل بلغة Swift، يستبدل كل تلك التعقيدات بواجهة برمجية أنيقة تعتمد على الماكرو (macros) ونظام الأنواع القوي في Swift. ومع كل إصدار جديد — من iOS 17 إلى iOS 26 — يزداد SwiftData نضجًا وقوة بشكل ملحوظ.

في هذا الدليل، سنستكشف SwiftData من الصفر حتى الأنماط المتقدمة، بما في ذلك ميزة وراثة النماذج (Model Inheritance) الجديدة في iOS 26. سواء كنت تبدأ مشروعًا جديدًا أو تفكّر في الانتقال من Core Data، هذا الدليل سيكون مرجعك العملي. هيا بنا نبدأ!

البنية الأساسية لـ SwiftData: الأركان الثلاثة

قبل أن نغوص في الأكواد، خلّنا نفهم البنية المعمارية لـ SwiftData. النظام يتكوّن من ثلاثة مكونات أساسية تعمل معًا بتناغم.

أولًا: Schema — تعريف نموذج البيانات

الـ Schema هو ببساطة وصف هيكل بياناتك. في SwiftData، تُعرّف النماذج باستخدام ماكرو @Model مباشرة على أصناف Swift العادية — بدون أي حاجة لملفات .xcdatamodeld أو واجهات رسومية:

import SwiftData

@Model
class Article {
    var title: String
    var content: String
    var publishedDate: Date
    var isPublished: Bool
    var viewCount: Int

    // العلاقة مع الكاتب
    var author: Author?

    // العلاقة مع الوسوم (متعدد لمتعدد)
    var tags: [Tag]

    init(title: String, content: String, publishedDate: Date = .now) {
        self.title = title
        self.content = content
        self.publishedDate = publishedDate
        self.isPublished = false
        self.viewCount = 0
        self.tags = []
    }
}

لاحظ البساطة هنا! لا يوجد NSManagedObject، لا @NSManaged، لا ملفات تكوين. مجرد صنف Swift عادي مع ماكرو @Model. الماكرو يتولّى تلقائيًا تحويل الصنف إلى كيان قابل للتخزين مع كل الخصائص اللازمة. (أليس هذا رائعًا؟)

ثانيًا: ModelContainer — حاوية البيانات

ModelContainer هو المسؤول عن إنشاء وإدارة ملف قاعدة البيانات الفعلي (SQLite تحت الغطاء). يمكنك تشبيهه بالمحرّك اللي يُشغّل كل شيء:

// الطريقة الأبسط — في نقطة دخول التطبيق
@main
struct MyBlogApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Article.self, Author.self, Tag.self])
    }
}

سطر واحد فقط! يُنشئ قاعدة بيانات محلية تدعم جميع النماذج المحدّدة. والجميل إن SwiftData ذكي بما يكفي لاكتشاف العلاقات بين النماذج تلقائيًا.

ثالثًا: ModelContext — سياق العمليات

ModelContext هو الواجهة اللي ستتعامل معها يوميًا كمطوّر. يتتبّع جميع التعديلات — الإنشاء، التحديث، الحذف — في الذاكرة قبل حفظها إلى القرص:

struct ArticleEditorView: View {
    @Environment(\.modelContext) private var context

    func createArticle() {
        let article = Article(
            title: "مقال جديد",
            content: "محتوى المقال هنا..."
        )
        context.insert(article)
        // SwiftData يحفظ تلقائيًا عند الحاجة
        // أو يمكنك الحفظ يدويًا:
        try? context.save()
    }

    func deleteArticle(_ article: Article) {
        context.delete(article)
    }
}

النقطة المهمة هنا إن SwiftData يدعم الحفظ التلقائي (autosave). في معظم الحالات، لن تحتاج لاستدعاء save() يدويًا — النظام يتكفّل بذلك. لكن في بعض السيناريوهات الحرجة (مثل قبل إغلاق التطبيق مباشرة)، الحفظ اليدوي يكون ضروريًا.

تعريف النماذج باحتراف: سمات وعلاقات وقواعد

طيب، الآن خلّنا نتعمّق أكثر في كيفية بناء نماذج بيانات غنية ومتكاملة.

السمات المخصصة باستخدام @Attribute

ماكرو @Attribute يمنحك تحكّمًا دقيقًا في كيفية تخزين كل خاصية. شخصيًا، أعتبر هذا من أكثر الأجزاء عملية في SwiftData:

@Model
class User {
    // قيمة فريدة — لا يمكن تكرارها
    @Attribute(.unique)
    var email: String

    var displayName: String

    // تخزين البيانات الكبيرة خارجيًا لتحسين الأداء
    @Attribute(.externalStorage)
    var profileImageData: Data?

    // خاصية محسوبة — لا تُخزّن في قاعدة البيانات
    @Transient
    var fullGreeting: String {
        "مرحبًا، \(displayName)!"
    }

    // تحويل أنواع مخصصة
    @Attribute(.transformable(by: "ColorTransformer"))
    var favoriteColor: UIColor?

    init(email: String, displayName: String) {
        self.email = email
        self.displayName = displayName
    }
}

ملاحظة مهمة: إذا كنت تنوي مزامنة البيانات مع iCloud عبر CloudKit، فلا يمكنك استخدام @Attribute(.unique). CloudKit ببساطة لا يدعم قيود التفرّد، وستحتاج لإدارة ذلك يدويًا في منطق التطبيق.

العلاقات بين النماذج باستخدام @Relationship

العلاقات هي جوهر أي نظام بيانات حقيقي. وهنا SwiftData يتألّق فعلًا — البساطة والوضوح:

@Model
class Author {
    var name: String
    var bio: String

    // علاقة واحد لمتعدد مع حذف متتالي
    @Relationship(deleteRule: .cascade, inverse: \Article.author)
    var articles: [Article]

    init(name: String, bio: String) {
        self.name = name
        self.bio = bio
        self.articles = []
    }
}

@Model
class Tag {
    var name: String

    // علاقة متعدد لمتعدد
    @Relationship(inverse: \Article.tags)
    var articles: [Article]

    init(name: String) {
        self.name = name
        self.articles = []
    }
}

قواعد الحذف المتاحة:

  • .cascade: حذف الكائن الأب يحذف جميع الأبناء. مثالي للعلاقات التبعية مثل (كاتب → مقالات).
  • .nullify: حذف الأب يجعل المرجع null عند الأبناء. وهذا هو السلوك الافتراضي.
  • .deny: يمنع حذف الأب طالما لديه أبناء مرتبطون.
  • .noAction: لا يفعل شيئًا — استخدمه بحذر شديد لأنه قد يترك مراجع معلّقة (وثق بي، هذا ليس ممتعًا عند تصحيح الأخطاء).

الاستعلامات الذكية: @Query و#Predicate

صراحةً، من أقوى ما في SwiftData هو نظام الاستعلامات الآمن نوعيًا (type-safe). دعني أوضّح لك كيف يعمل.

الاستعلامات الأساسية مع @Query

struct ArticleListView: View {
    // جلب جميع المقالات مرتّبة حسب تاريخ النشر
    @Query(sort: \Article.publishedDate, order: .reverse)
    private var articles: [Article]

    var body: some View {
        List(articles) { article in
            ArticleRow(article: article)
        }
    }
}

هذا أبسط شكل ممكن. SwiftData يربط الاستعلام تلقائيًا بواجهة SwiftUI — أي تغيير في البيانات يُحدّث الواجهة فورًا بدون كود إضافي. مذهل، أليس كذلك؟

التصفية المتقدمة باستخدام #Predicate

ماكرو #Predicate هو البديل العصري لـ NSPredicate القديم. الفرق الجوهري؟ كل شيء مدقّق وقت الترجمة:

struct PublishedArticlesView: View {
    // جلب المقالات المنشورة فقط التي لديها أكثر من 100 مشاهدة
    @Query(
        filter: #Predicate
{ article in article.isPublished == true && article.viewCount > 100 }, sort: \Article.viewCount, order: .reverse ) private var popularArticles: [Article] var body: some View { List(popularArticles) { article in VStack(alignment: .leading) { Text(article.title) .font(.headline) Text("\(article.viewCount) مشاهدة") .font(.caption) .foregroundStyle(.secondary) } } } }

الجميل في #Predicate إنه يستخدم تعبيرات Swift العادية. أخطأت في اسم خاصية؟ المترجم يخبرك فورًا. نوع بيانات خاطئ؟ خطأ ترجمة واضح. وداعًا لأخطاء وقت التشغيل المحبطة مع NSPredicate!

الاستعلامات الديناميكية

في كثير من الأحيان، تحتاج لتغيير معايير الاستعلام بناءً على تفاعل المستخدم — مثل البحث أو التصفية. الطريقة المعتمدة هي إنشاء عرض فرعي:

struct SearchableArticlesView: View {
    @State private var searchText = ""

    var body: some View {
        NavigationStack {
            FilteredArticleList(searchText: searchText)
                .searchable(text: $searchText)
        }
    }
}

struct FilteredArticleList: View {
    @Query private var articles: [Article]

    init(searchText: String) {
        let predicate: Predicate
if searchText.isEmpty { predicate = #Predicate { _ in true } } else { predicate = #Predicate
{ article in article.title.localizedStandardContains(searchText) } } _articles = Query( filter: predicate, sort: \Article.publishedDate, order: .reverse ) } var body: some View { List(articles) { article in ArticleRow(article: article) } } }

النمط هنا هو إنشاء عرض فرعي (sub-view) يستقبل معايير البحث كمعامل في init ثم يُهيّئ @Query بناءً عليها. السبب؟ @Query لا يمكن تغييره بعد إنشاء العرض — لكن إنشاء عرض جديد بمعايير جديدة يُعيد تهيئة الاستعلام تلقائيًا. حيلة ذكية!

FetchDescriptor للاستعلامات البرمجية

عندما تحتاج لتنفيذ استعلامات خارج واجهة SwiftUI (في ViewModel أو خدمة بيانات مثلًا)، هنا يأتي دور FetchDescriptor:

func fetchRecentArticles(context: ModelContext) throws -> [Article] {
    var descriptor = FetchDescriptor
( predicate: #Predicate { $0.isPublished }, sortBy: [SortDescriptor(\Article.publishedDate, order: .reverse)] ) // تحديد عدد النتائج descriptor.fetchLimit = 20 // تحميل العلاقات مسبقًا لتحسين الأداء descriptor.relationshipKeyPathsForPrefetching = [\.author, \.tags] return try context.fetch(descriptor) } // حساب عدد النتائج بدون جلب البيانات func countPublishedArticles(context: ModelContext) throws -> Int { let descriptor = FetchDescriptor
( predicate: #Predicate { $0.isPublished } ) return try context.fetchCount(descriptor) }

لاحظ استخدام relationshipKeyPathsForPrefetching — هذه أداة أعتبرها ضرورية لتحسين الأداء. بدلًا من تحميل كل علاقة على حدة عند الوصول إليها (ما يُعرف بـ lazy loading)، يتم تحميلها دفعة واحدة مع الاستعلام الرئيسي. النتيجة؟ استعلامات أقل لقاعدة البيانات وأداء أفضل بكثير.

إعدادات متقدمة لـ ModelContainer

في التطبيقات الحقيقية، غالبًا ستحتاج لتخصيص حاوية البيانات بشكل أعمق من الإعداد الافتراضي.

تكوين مخصص باستخدام ModelConfiguration

@main
struct MyBlogApp: App {
    let container: ModelContainer

    init() {
        let schema = Schema([
            Article.self,
            Author.self,
            Tag.self
        ])

        let config = ModelConfiguration(
            "MyBlogDB",              // اسم قاعدة البيانات
            schema: schema,
            isStoredInMemoryOnly: false,  // true للاختبارات
            allowsSave: true,
            groupContainer: .automatic    // لمشاركة البيانات مع App Groups
        )

        do {
            container = try ModelContainer(
                for: schema,
                configurations: [config]
            )
        } catch {
            fatalError("فشل في إنشاء حاوية البيانات: \(error)")
        }
    }

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

مزامنة البيانات مع iCloud عبر CloudKit

من أقوى ميزات SwiftData هي المزامنة السحابية المدمجة. والمفاجأة؟ الإعداد بسيط بشكل لا يُصدّق:

let config = ModelConfiguration(
    cloudKitContainerIdentifier: "iCloud.com.mycompany.myblogapp"
)

let container = try ModelContainer(
    for: Article.self, Author.self, Tag.self,
    configurations: config
)

لكن (وهنا الجزء المهم) هناك متطلبات يجب مراعاتها عند استخدام CloudKit:

  • لا يمكن استخدام @Attribute(.unique) — CloudKit لا يدعم قيود التفرّد.
  • جميع الخصائص يجب أن تحتوي على قيم افتراضية أو تكون اختيارية.
  • جميع العلاقات يجب أن تكون اختيارية (optional).
  • يجب إضافة قدرة iCloud و Background Modes (Remote Notifications) في إعدادات المشروع.

شيء مهم أيضًا: المزامنة السحابية ليست فورية دائمًا. قد تستغرق بضع ثوانٍ إلى بضع دقائق حسب الشبكة. صمّم واجهتك بحيث تتعامل مع هذا التأخير بسلاسة — مثلًا أضف مؤشر تحميل أو رسالة "جارٍ المزامنة".

ترحيل مخطط البيانات: VersionedSchema وSchemaMigrationPlan

مع تطوّر تطبيقك، يتطوّر نموذج بياناتك حتمًا. إضافة خصائص جديدة، تغيير أنواع البيانات، إعادة هيكلة العلاقات... كل هذا يحتاج إدارة ذكية. ولحسن الحظ، SwiftData يوفّر نظامًا متكاملًا لذلك.

تعريف إصدارات المخطط

// الإصدار الأول من المخطط
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Article.self]
    }

    @Model
    class Article {
        var title: String
        var content: String
        var createdDate: Date

        init(title: String, content: String) {
            self.title = title
            self.content = content
            self.createdDate = .now
        }
    }
}

// الإصدار الثاني — إضافة خاصية isPublished وتغيير اسم خاصية
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Article.self]
    }

    @Model
    class Article {
        var title: String
        var content: String
        var publishedDate: Date    // تم تغيير الاسم من createdDate
        var isPublished: Bool      // خاصية جديدة

        init(title: String, content: String) {
            self.title = title
            self.content = content
            self.publishedDate = .now
            self.isPublished = false
        }
    }
}

تعريف خطة الترحيل

enum BlogMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    // ترحيل خفيف الوزن — SwiftData يتعامل معه تلقائيًا
    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )
}

// إذا كان الترحيل معقّدًا (مثل دمج بيانات مكررة)
enum ComplexMigrationPlan: 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
            // منطق مخصص قبل الترحيل
            // مثال: إزالة المقالات المكررة
            let articles = try context.fetch(
                FetchDescriptor()
            )

            var seenTitles: Set = []
            for article in articles {
                if seenTitles.contains(article.title) {
                    context.delete(article)
                } else {
                    seenTitles.insert(article.title)
                }
            }
            try context.save()
        },
        didMigrate: nil
    )
}

ثم استخدم خطة الترحيل عند إنشاء الحاوية:

let container = try ModelContainer(
    for: SchemaV2.Article.self,
    migrationPlan: BlogMigrationPlan.self
)

نصيحة من تجربة شخصية: اختبر عمليات الترحيل دائمًا قبل إصدار أي تحديث. أنشئ اختبارات وحدة تفتح قاعدة بيانات بالإصدار القديم وتتحقّق من نجاح الترحيل وسلامة البيانات. تصدّقني، هذا الاختبار ينقذك من مشاكل كبيرة لاحقًا.

وراثة النماذج في iOS 26: الميزة الجديدة المنتظرة

واحدة من أبرز إضافات WWDC 2025 هي دعم وراثة النماذج (Model Inheritance) في SwiftData. صراحةً، هذه الميزة كانت مطلوبة من يوم إطلاق SwiftData، وأخيرًا وصلت!

متى تحتاج وراثة النماذج؟

تخيّل معي: تطبيق سفر يتتبّع أنواعًا مختلفة من الرحلات — رحلات عمل، رحلات سياحية، رحلات عائلية. كل نوع يشترك في خصائص أساسية (الوجهة، تاريخ البدء، تاريخ الانتهاء)، لكن لكل نوع خصائص إضافية خاصة به.

بدون وراثة النماذج، كنت ستواجه أحد حلّين سيّئين: إما نموذج واحد ضخم مليء بالخصائص الاختيارية (فوضى!)، أو نماذج منفصلة تمامًا مع تكرار كود كثير. كلا الحلّين ليسا مثاليين.

التطبيق العملي

// النموذج الأساسي
@available(iOS 26, *)
@Model
class Trip {
    var destination: String
    var startDate: Date
    var endDate: Date
    var notes: String

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

// رحلة عمل ترث من Trip
@available(iOS 26, *)
@Model
class BusinessTrip: Trip {
    var companyName: String
    var meetingAgenda: String
    var expenseBudget: Decimal

    init(destination: String, startDate: Date, endDate: Date,
         companyName: String, meetingAgenda: String, expenseBudget: Decimal) {
        self.companyName = companyName
        self.meetingAgenda = meetingAgenda
        self.expenseBudget = expenseBudget
        super.init(destination: destination, startDate: startDate, endDate: endDate)
    }
}

// رحلة سياحية ترث من Trip
@available(iOS 26, *)
@Model
class VacationTrip: Trip {
    var activities: [String]
    var accommodation: String
    var isAllInclusive: Bool

    init(destination: String, startDate: Date, endDate: Date,
         activities: [String], accommodation: String, isAllInclusive: Bool) {
        self.activities = activities
        self.accommodation = accommodation
        self.isAllInclusive = isAllInclusive
        super.init(destination: destination, startDate: startDate, endDate: endDate)
    }
}

الاستعلام مع الوراثة

ميزة رائعة: الاستعلام عن النوع الأساسي يُرجع تلقائيًا جميع الأنواع الفرعية أيضًا!

// هذا يُرجع جميع الرحلات — عمل، سياحة، وأي نوع آخر
@Query(sort: \Trip.startDate)
private var allTrips: [Trip]

// يمكنك أيضًا الاستعلام عن نوع فرعي محدد فقط
@Query(sort: \BusinessTrip.startDate)
private var businessTrips: [BusinessTrip]

ولعرض الرحلات حسب نوعها في الواجهة:

@available(iOS 26, *)
struct TripListView: View {
    @Query(sort: \Trip.startDate, order: .reverse)
    private var trips: [Trip]

    var body: some View {
        List(trips) { trip in
            switch trip {
            case let businessTrip as BusinessTrip:
                BusinessTripRow(trip: businessTrip)
            case let vacation as VacationTrip:
                VacationTripRow(trip: vacation)
            default:
                GenericTripRow(trip: trip)
            }
        }
    }
}

تنبيه مهم: وراثة النماذج متاحة فقط في iOS 26 وما بعده. إذا كان تطبيقك يدعم إصدارات أقدم، يجب استخدام @available(iOS 26, *) على جميع الأصناف الفرعية. قد يكون هذا تحديًا حقيقيًا إذا كانت هذه النماذج أساسية في تطبيقك — وفي هذه الحالة ربما الأفضل الانتظار حتى تتمكّن من رفع الحد الأدنى المدعوم.

أنماط عملية وأفضل الممارسات

الأساسيات والميزات المتقدمة أصبحت واضحة. الآن دعنا ننتقل للجزء العملي — الأنماط اللي ستستخدمها فعلًا في تطبيقاتك.

نمط Repository لفصل منطق البيانات

@Observable
class ArticleRepository {
    private let context: ModelContext

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

    func fetchAll(
        sortBy: KeyPath = \.publishedDate,
        ascending: Bool = false
    ) throws -> [Article] {
        let descriptor = FetchDescriptor
( sortBy: [SortDescriptor(sortBy, order: ascending ? .forward : .reverse)] ) return try context.fetch(descriptor) } func search(query: String) throws -> [Article] { let descriptor = FetchDescriptor
( predicate: #Predicate { $0.title.localizedStandardContains(query) || $0.content.localizedStandardContains(query) } ) return try context.fetch(descriptor) } func create(title: String, content: String, author: Author) -> Article { let article = Article(title: title, content: content) article.author = author context.insert(article) return article } func delete(_ article: Article) { context.delete(article) } func save() throws { try context.save() } }

هذا النمط يفصل منطق الوصول للبيانات عن واجهة المستخدم، وهذا يُسهّل الاختبار بشكل كبير. يمكنك حقن هذا المستودع في عروض SwiftUI عبر البيئة (Environment) بكل سهولة.

العمليات المجمّعة (Batch Operations)

عند التعامل مع كميات كبيرة من البيانات، تجنّب إدراج أو حذف العناصر واحدًا تلو الآخر. هذا خطأ شائع يؤثر بشكل واضح على الأداء:

func importArticles(from jsonData: Data, context: ModelContext) throws {
    let decoder = JSONDecoder()
    let importedArticles = try decoder.decode([ArticleDTO].self, from: jsonData)

    // إدراج مجمّع
    for dto in importedArticles {
        let article = Article(
            title: dto.title,
            content: dto.content,
            publishedDate: dto.date
        )
        article.isPublished = dto.published
        context.insert(article)
    }

    // حفظ واحد بعد كل الإدراجات
    try context.save()
}

// الحذف المجمّع
func deleteAllDrafts(context: ModelContext) throws {
    try context.delete(
        model: Article.self,
        where: #Predicate { $0.isPublished == false }
    )
}

العمل في الخلفية (Background Context)

للعمليات الثقيلة التي قد تجمّد الواجهة، استخدم سياقًا منفصلًا في الخلفية:

func performHeavyImport(container: ModelContainer) async throws {
    // إنشاء سياق خلفية منفصل عن الـ Main Actor
    let backgroundContext = ModelContext(container)

    // العمليات الثقيلة هنا
    let articles = try await fetchArticlesFromAPI()

    for article in articles {
        let model = Article(
            title: article.title,
            content: article.content
        )
        backgroundContext.insert(model)
    }

    try backgroundContext.save()
}

نقطة مهمة جدًا يغفل عنها كثيرون: كائنات SwiftData مرتبطة بالسياق الذي أنشأتها فيه. لا يمكنك تمرير كائن من سياق الخلفية إلى واجهة SwiftUI مباشرة — بدلًا من ذلك، أعد جلب البيانات من السياق الرئيسي بعد انتهاء العملية.

اختبار النماذج والاستعلامات

SwiftData يُسهّل كتابة الاختبارات بفضل خيار التخزين في الذاكرة — لا ملفات مؤقتة ولا تنظيف بعد الاختبار:

import Testing
import SwiftData

@Test
func testArticleCreation() throws {
    // إنشاء حاوية في الذاكرة — لا تؤثر على البيانات الحقيقية
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try ModelContainer(
        for: Article.self, Author.self, Tag.self,
        configurations: config
    )
    let context = ModelContext(container)

    // إنشاء كاتب ومقال
    let author = Author(name: "أحمد", bio: "مطوّر iOS")
    context.insert(author)

    let article = Article(title: "مقال اختباري", content: "محتوى")
    article.author = author
    context.insert(article)

    try context.save()

    // التحقّق
    let descriptor = FetchDescriptor
() let articles = try context.fetch(descriptor) #expect(articles.count == 1) #expect(articles.first?.title == "مقال اختباري") #expect(articles.first?.author?.name == "أحمد") }

الانتقال من Core Data إلى SwiftData

إذا كان لديك تطبيق قائم يستخدم Core Data، فالسؤال الطبيعي: كيف أنتقل؟ إليك استراتيجية عملية مجرّبة.

النهج التدريجي (وهو الموصى به بقوة)

لا تحاول إعادة كتابة كل شيء دفعة واحدة. بدلًا من ذلك، انتقل خطوة بخطوة:

  1. الخطوة الأولى: استخدم أداة Xcode المدمجة — افتح ملف .xcdatamodeld ثم اذهب إلى Editor → Create SwiftData Code لتحويل النماذج تلقائيًا.
  2. الخطوة الثانية: راجع الكود المُنشأ وحسّنه. أزل الاختيارية (optionality) حيثما أمكن وأضف قيمًا افتراضية.
  3. الخطوة الثالثة: استبدل @FetchRequest بـ @Query في العروض الجديدة أو المُعاد كتابتها.
  4. الخطوة الرابعة: استبدل managedObjectContext بـ modelContext في البيئة.
  5. الخطوة الخامسة: اختبر كل تغيير بشكل مستقل قبل الانتقال للخطوة التالية.

التعايش المشترك

خبر جيد: يمكن لـ Core Data وSwiftData التعايش في نفس التطبيق ومشاركة نفس ملف SQLite! هذا يعني أنك تستطيع ترحيل الشاشات واحدة تلو الأخرى بدون أي ضغط:

// الحاوية المشتركة التي تدعم كلا الإطارين
let url = URL.applicationSupportDirectory
    .appending(path: "MyApp.sqlite")

// SwiftData يصل لنفس الملف
let config = ModelConfiguration(url: url)
let swiftDataContainer = try ModelContainer(
    for: Article.self,
    configurations: config
)

// Core Data يصل لنفس الملف أيضًا
let coreDataContainer = NSPersistentContainer(name: "MyApp")
coreDataContainer.persistentStoreDescriptions.first?.url = url
coreDataContainer.loadPersistentStores { _, error in
    if let error { fatalError("خطأ: \(error)") }
}

متى تستخدم SwiftData ومتى تبقى مع Core Data؟

هذا السؤال يتكرّر كثيرًا في مجتمع مطوّري iOS. إليك رأيي المبني على الخبرة العملية.

استخدم SwiftData إذا:

  • تبدأ مشروعًا جديدًا والحد الأدنى هو iOS 17 أو أحدث.
  • نموذج بياناتك بسيط إلى متوسط التعقيد.
  • تستخدم SwiftUI بشكل أساسي — التكامل سلس جدًا.
  • تُقدّر سرعة التطوير وإنتاجية المطوّر.
  • تحتاج مزامنة CloudKit بأقل جهد ممكن.

ابقَ مع Core Data إذا:

  • تطبيقك يدعم إصدارات أقدم من iOS 17.
  • تحتاج عمليات ترحيل معقّدة ومخصّصة (heavyweight migrations).
  • تتعامل مع بيانات ضخمة جدًا وتحتاج تحسينات أداء دقيقة.
  • تعتمد بشكل كبير على UIKit مع NSFetchedResultsController.
  • لديك بنية تحتية معقّدة حول Core Data بدون مبرر تجاري كافٍ للترحيل.

رأيي الشخصي؟ في عام 2025 وما بعده، SwiftData هو الخيار الأنسب لحوالي 80% من المشاريع الجديدة. الإطار نضج بشكل واضح والفجوة مع Core Data تضيق مع كل إصدار.

نصائح لتحسين الأداء

قبل أن نختم، إليك مجموعة نصائح عملية مهمة لضمان أداء ممتاز في تطبيقاتك:

  • استخدم fetchLimit دائمًا: لا تجلب أكثر مما تحتاج. إذا تعرض 20 عنصرًا، حدّد fetchLimit = 20.
  • استخدم fetchCount بدلًا من fetch: إذا كنت تحتاج فقط للعدد، لا داعي لجلب كل الكائنات.
  • استخدم @Attribute(.externalStorage) للبيانات الكبيرة: الصور والملفات يجب أن تُخزّن خارجيًا.
  • فعّل التحميل المسبق للعلاقات: relationshipKeyPathsForPrefetching يحل مشكلة N+1 الشهيرة.
  • نفّذ العمليات الثقيلة في الخلفية: لا تحظر الـ Main Thread بعمليات مجمّعة.
  • استخدم @Transient للخصائص المحسوبة: لا تُخزّن ما يمكن حسابه.
  • اختبر مع بيانات واقعية الحجم: الأداء الجيد مع 100 عنصر لا يعني بالضرورة أداءً جيدًا مع 100,000 عنصر!

الخلاصة

SwiftData يمثّل فعلًا نقلة نوعية في طريقة تعامل مطوّري Apple مع البيانات المحلية. من بساطة تعريف النماذج بـ @Model، إلى الاستعلامات الآمنة نوعيًا مع #Predicate، إلى المزامنة السحابية المدمجة، إلى وراثة النماذج في iOS 26 — كل شيء مصمّم ليجعل حياتك كمطوّر أسهل.

إذا كنت تبدأ مشروعًا جديدًا اليوم، SwiftData هو الخيار الواضح. وإذا كان لديك تطبيق Core Data قائم، ابدأ بالتعايش التدريجي وانتقل خطوة بخطوة.

الأهم؟ ابدأ الآن. أنشئ تطبيقًا صغيرًا، جرّب النماذج والاستعلامات، ثم توسّع تدريجيًا. ستكتشف أن ما كان يتطلّب عشرات الأسطر في Core Data يمكن إنجازه بأسطر معدودة — وبثقة أكبر بفضل الأمان النوعي الذي يوفّره مترجم Swift.

عن الكاتب Editorial Team

Our team of expert writers and editors.