SwiftData ב-iOS 26: המדריך המלא לפרסיסטנס ב-SwiftUI (מיגרציה מ-Core Data)

מדריך מלא ל-SwiftData ב-iOS 26: הגדרת מודלים עם @Model, שאילתות ריאקטיביות עם @Query, קשרים, מיגרציית סכימה, סנכרון CloudKit, ומעבר מ-Core Data. כולל דוגמאות קוד עובדות בפרודקשן.

SwiftData ב-iOS 26: המדריך המלא (2026)

עודכן: 17 באוגוסט, 2026

SwiftData ב-iOS 26 הוא מסגרת הפרסיסטנס המקורית של Apple ל-SwiftUI, שמחליפה את Core Data באמצעות מקרואים (macros) של Swift במקום קבצי xcdatamodeld ויצירת קוד ב-build time. המסגרת מזהה את הסכימה שלך ישירות מתוך מחלקות Swift שמוסמנות ב-@Model, מסנכרנת אוטומטית ל-CloudKit, ומספקת שאילתות ריאקטיביות ל-SwiftUI דרך @Query. אז בואו נצלול פנימה. במדריך הזה אני מראה לך איך מהנדסים ב-2026 מיישמים SwiftData בפרויקטים פרודקשן, כולל מיגרציה מ-Core Data שהזעיקה אותי שעה לפני שחרור בפרויקט אחד שלי, ניהול קשרים, וסכימת גרסאות.

  • SwiftData ב-iOS 26 מחליף את Core Data ומקטין בין 40% ל-60% בקוד ה-boilerplate בזכות המקרו @Model.
  • המקרו @Query מחזיר [Element] שמתעדכן אוטומטית כשה-ModelContext משתנה, בלי @FetchRequest או NSFetchedResultsController.
  • קשרים (relationships) מוגדרים כתכונות רגילות של Swift עם המקרו @Relationship, כולל כלל מחיקה מסוג cascade או nullify.
  • מיגרציית סכימה מבוצעת דרך VersionedSchema ו-SchemaMigrationPlan, עם תמיכה בשלבים lightweight ו-custom.
  • סנכרון CloudKit דורש שדות אופציונליים או ערכי ברירת מחדל, ותכונות ייחודיות (@Attribute(.unique)) לא נתמכות בקונטיינרים ציבוריים.
  • SwiftData מבוסס על Core Data מתחת למכסה, כך שניתן לשלב את שתי המסגרות באותה אפליקציה במהלך המיגרציה.

מה זה SwiftData ובמה הוא שונה מ-Core Data

SwiftData היא מסגרת הצהרתית לפרסיסטנס שהוצגה ב-WWDC 2023 והתייצבה כברירת מחדל ב-iOS 26. במקום עורך גרפי של xcdatamodeld ו-NSManagedObject subclasses שנוצרים אוטומטית, מגדירים את המודל כמחלקת Swift רגילה שמסומנת במקרו @Model. המקרו מזרים בזמן קומפילציה את התכונות, מייצר את הסכימה, ומאפשר ל-SwiftUI לצפות בשינויים ללא צורך ב-ObservableObject.

ההבדל המרכזי בעבודה יומיומית הוא שאין יותר סטרינגים קסומים. ב-Core Data קראתי לרוב ל-value(forKey: "title") או השתמשתי ב-@NSManaged; ב-SwiftData אני פשוט כותב book.title כמו על struct רגיל. הקומפיילר תופס טעויות שם, refactor ידידותי יותר, וה-diff ב-Git הופך לקריא. וזה לא רק נחמד לעין: זה חסך לי באג מטופש שרדף אותי חצי יום כשעברתי בין סניפים.

מבחינת ארכיטקטורה, SwiftData יושב על Core Data מתחת למכסה. זו לא מסגרת חדשה מאפס, אלא שכבה חדשה עם API מודרני. משמעות הדבר: הביצועים דומים, אתה יכול לפתוח בסיס נתונים של SwiftData גם דרך Core Data במהלך המיגרציה, ותכונות חסרות כמו fetchLimit יכולות להיגזר משאילתה ידנית ל-NSFetchRequest במקרים קיצוניים.

שני שינויים חשובים ב-iOS 26 שכדאי להכיר: ראשית, המקרו @Query תומך כעת ב-animation: ישירות, ללא צורך לעטוף את השינוי ב-withAnimation. שנית, יצירת Predicate מקבלת מפעילים מורכבים יותר כמו contains עם caseSensitive: false, מה שפותר בעיה שהעצבנה מפתחים במהלך שנתיים.

הגדרת מודלים עם @Model

המקרו @Model ממיר מחלקה רגילה של Swift למחלקה נשמרת. בשונה מ-struct, המודל חייב להיות class מכיוון ש-SwiftData מסתמך על זהות התייחסות (reference identity) כדי לעקוב אחרי שינויים. הנה מודל מלא לספר:

import SwiftData
import Foundation

@Model
final class Book {
    // Attribute with a unique constraint - prevents duplicate ISBNs
    @Attribute(.unique) var isbn: String
    var title: String
    var author: String
    var pageCount: Int
    var publishedAt: Date
    var isRead: Bool

    // Optional attribute - useful for gradual rollout of new fields
    var summary: String?

    // Relationship - deleting the book removes its notes as well
    @Relationship(deleteRule: .cascade, inverse: \Note.book)
    var notes: [Note] = []

    init(isbn: String, title: String, author: String,
         pageCount: Int, publishedAt: Date) {
        self.isbn = isbn
        self.title = title
        self.author = author
        self.pageCount = pageCount
        self.publishedAt = publishedAt
        self.isRead = false
    }
}

@Model
final class Note {
    var body: String
    var createdAt: Date
    var book: Book?

    init(body: String, book: Book? = nil) {
        self.body = body
        self.createdAt = .now
        self.book = book
    }
}

שימו לב לשלוש נקודות שכדאי לשים לב אליהן: final class חובה, מפני שירושה בין מודלי SwiftData מוגבלת ב-iOS 26 ופוגעת בביצועי fetch. @Attribute(.unique) משמש למניעת כפילויות ב-ISBN - בעת ניסיון להכניס כפילות, SwiftData מעדכן את הרשומה הקיימת במקום לזרוק שגיאה, התנהגות שנקראת upsert. תחילת מערך ה-notes ב-[] חשובה: אם משאירים אותו לא-אתחל, קריאה ראשונה תזרוק runtime crash.

ModelContainer ו-ModelContext: הבסיס לכל אפליקציה

ה-ModelContainer הוא המקבילה של NSPersistentContainer: אובייקט יחיד לאפליקציה שמנהל את קובץ הבסיס הפיזי. ה-ModelContext הוא unit-of-work: מקום שבו שינויים מצטברים לפני שנשמרים. במרבית האפליקציות רושמים את הקונטיינר ב-App ומאפשרים ל-SwiftUI להזריק את ההקשר לכל תצוגה.

import SwiftUI
import SwiftData

@main
struct BookshelfApp: App {
    let container: ModelContainer

    init() {
        do {
            let schema = Schema([Book.self, Note.self])
            let config = ModelConfiguration(
                schema: schema,
                isStoredInMemoryOnly: false,
                cloudKitDatabase: .automatic
            )
            container = try ModelContainer(
                for: schema,
                configurations: [config]
            )
        } catch {
            // Fail fast - a broken container means the app cannot function
            fatalError("Failed to create ModelContainer: \(error)")
        }
    }

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

בתוך תצוגה, קוראים ל-@Environment(\.modelContext) private var context ומוסיפים או מוחקים אובייקטים דרך context.insert(book) ו-context.delete(book). שינויים נשמרים אוטומטית ברקע כשהתצוגה נעלמת מהמסך, אבל שמירה מפורשת דרך try context.save() חשובה כאשר אתה מבצע פעולות מרובות ורוצה שהן ייכתבו לדיסק לפני שהמשתמש עוזב את המסך.

עבור פעולות רקע כבדות - למשל, ייבוא של 50,000 רשומות - צור ModelContext נפרד בתוך Task.detached ואל תשתמש בזה של הסביבה. הסיבה: ה-context של הסביבה קשור ל-MainActor, ופעולות המוניות עליו יגרמו ל-frame drops. הגישה הזו זהה לרוח למה שעשיתי עם backgroundContext ב-Core Data, אך עם API נקי בהרבה.

שאילתות ריאקטיביות עם @Query ו-#Predicate

הרווח הגדול ביותר של SwiftUI + SwiftData הוא המקרו @Query, שמשלב @FetchRequest ו-ObservableObject ל-property wrapper אחד. שאילתה בסיסית לרשימת ספרים ממוינת נראית כך:

struct BookListView: View {
    @Query(sort: \Book.title, order: .forward) private var books: [Book]
    @Environment(\.modelContext) private var context

    var body: some View {
        List(books) { book in
            NavigationLink(book.title) {
                BookDetailView(book: book)
            }
        }
        .toolbar {
            Button("Add sample") {
                let book = Book(
                    isbn: "978-0135166307",
                    title: "Clean Architecture",
                    author: "Robert C. Martin",
                    pageCount: 432,
                    publishedAt: .now
                )
                context.insert(book)
            }
        }
    }
}

לפילטרים דינמיים - כמו חיפוש טקסטואלי - יש לעטוף את @Query ב-view משנה שמקבל את הטקסט כפרמטר של האתחול. ה-#Predicate DSL ב-iOS 26 תומך בביטויים מורכבים:

struct FilteredBookList: View {
    @Query private var books: [Book]

    init(searchText: String, showOnlyUnread: Bool) {
        let predicate = #Predicate { book in
            (searchText.isEmpty ||
             book.title.localizedStandardContains(searchText)) &&
            (!showOnlyUnread || !book.isRead)
        }
        _books = Query(filter: predicate, sort: \.publishedAt, order: .reverse)
    }

    var body: some View {
        List(books) { Text($0.title) }
    }
}

יש לשים לב לשתי מלכודות: ראשית, #Predicate מתורגם ל-SQL על ידי הליבה של Core Data, ולא כל פעולת Swift נתמכת. לדוגמה, קריאה ל-computed property בתוך predicate תזרוק שגיאת runtime. שנית, שאילתות עם fetchLimit קטן על מסכי רשימה ארוכים עדיפות על שאילתת "הכל" עם lazyStack - השני יטען את כל האובייקטים לזיכרון.

קשרים בין מודלים ו-@Relationship

SwiftData תומך בכל סוגי הקשרים המוכרים מ-Core Data: אחד-לאחד, אחד-לרבים, ורבים-לרבים. ההגדרה מתבצעת עם המקרו @Relationship, שמקבל שני פרמטרים חשובים: deleteRule ו-inverse. הגדרה נכונה של inverse מונעת בעיות של יתמות (orphaned records) ומחזקת את שלמות הבסיס.

@Model
final class Author {
    var name: String
    var country: String

    // Many-to-many: an author can have several genres,
    // and a genre lists many authors
    @Relationship(inverse: \Genre.authors)
    var genres: [Genre] = []

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

@Model
final class Genre {
    @Attribute(.unique) var name: String
    var authors: [Author] = []

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

ארבעת כללי המחיקה שכדאי להכיר: .cascade - מוחק את האובייקטים הקשורים; .nullify - מנתק את הקשר אך משאיר את האובייקטים; .deny - מונע מחיקה אם קיימים קשרים; .noAction - משאיר את האובייקטים הקשורים בלי לעדכן אותם (זהירות: יוצר יתומים). ברוב האפליקציות שלי, cascade משמש לתת-אובייקטים (הערות של ספר), ו-nullify משמש לאובייקטים משותפים (מחבר של כמה ספרים).

לביצועים: קשרים נטענים ב-lazy כברירת מחדל. בעת גישה ראשונה לתכונת קשר, SwiftData מבצע fetch נוסף. אם אתה יודע מראש שאתה צריך את הקשר, בקש אותו במפורש בעזרת fetchDescriptor.relationshipKeyPathsForPrefetching. זה חוסך את בעיית ה-N+1 שהורגת מסכי רשימה. הכלי הזה זהה ל-relationshipKeyPathsForPrefetching ב-Core Data, אך כאן הוא type-safe.

איך לבצע מיגרציית סכימה ב-SwiftData

שינויי סכימה הם מציאות של כל אפליקציה: הוספת שדה חדש, שינוי שם של תכונה, פיצול מודל לשניים. SwiftData מטפל אוטומטית במיגרציות lightweight - הוספת שדות אופציונליים, הוספת מודלים חדשים, שינויים לא-הרסניים. עבור שינויים מבניים, מגדירים VersionedSchema ו-SchemaMigrationPlan.

enum BookSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Book.self] }

    @Model
    final class Book {
        @Attribute(.unique) var isbn: String
        var title: String
        var author: String  // single string in V1

        init(isbn: String, title: String, author: String) {
            self.isbn = isbn; self.title = title; self.author = author
        }
    }
}

enum BookSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [Book.self, Author.self] }

    @Model
    final class Book {
        @Attribute(.unique) var isbn: String
        var title: String
        var authorRef: Author?  // now a relationship
        init(isbn: String, title: String) {
            self.isbn = isbn; self.title = title
        }
    }
    // Author model definition...
}

enum BookMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [BookSchemaV1.self, BookSchemaV2.self]
    }
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }

    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: BookSchemaV1.self,
        toVersion: BookSchemaV2.self,
        willMigrate: { context in
            // Read V1 authors as strings and create Author entities
            let oldBooks = try context.fetch(FetchDescriptor<BookSchemaV1.Book>())
            for old in oldBooks {
                let author = BookSchemaV2.Author(name: old.author)
                context.insert(author)
            }
            try context.save()
        },
        didMigrate: { context in
            // Link books to their new Author entities
        }
    )
}

בעת אתחול הקונטיינר, מספקים את ה-migration plan: ModelContainer(for: schema, migrationPlan: BookMigrationPlan.self, configurations: [config]). מומלץ מאוד לגבות את הבסיס לפני מיגרציה custom - כישלון באמצע מיגרציה ישאיר את המשתמש עם בסיס נתונים שבור. לפרטים על התהליך המלא ראה את התיעוד הרשמי של SchemaMigrationPlan.

סנכרון עם CloudKit ותאימות רב-מכשירית

סנכרון CloudKit ב-SwiftData מתבצע דרך cloudKitDatabase: .automatic ב-ModelConfiguration. Apple יוצרת מאחורי הקלעים CKRecord מקביל לכל PersistentModel, מפעילה subscriptions על שינויים, ומטפלת בקונפליקטים בשיטת last-write-wins. אבל יש שלושה כללים נוקשים שכדאי לזכור:

  • כל שדה חייב להיות אופציונלי או בעל ערך ברירת מחדל. CloudKit לא תומכת בהוספת שדה חובה חדש בלי migration מיוחד.
  • המקרו @Attribute(.unique) לא נתמך בקונטיינרים ציבוריים של CloudKit - יש להשתמש בקונטיינר פרטי, או לוותר על ייחודיות ולנהל אותה בקוד.
  • כל @Relationship חייב להיות אופציונלי או מערך. לא ניתן להגדיר קשר חובה של אחד-לאחד.

בפרויקטים אמיתיים אני מפריד בין entities שנשמרות רק מקומית לבין entities שסונכרנות. עבור מטמון של תמונות thumbnail, למשל, הגדרתי ModelConfiguration נפרד עם cloudKitDatabase: .none, ואז ה-container מנהל שני stores נפרדים בו זמנית. זה חוסך רוחב פס של CloudKit ומקטין קונפליקטים.

לדיבאג של בעיות סנכרון, הפעל את משתני הסביבה com.apple.CoreData.CloudKitDebug=1 ב-scheme, וצפה בלוגים דרך Console.app. הבעיות הנפוצות שאני נתקל בהן: שדות חובה שנשכחו, ADMIN של iCloud שלא מסומן, וסכימת CloudKit שלא הועברה מ-Development ל-Production ב-CloudKit Dashboard.

מיגרציה מ-Core Data ל-SwiftData

בכנות, מיגרציה מ-Core Data ל-SwiftData היא אחת השאלות שאני נשאל בהן הכי הרבה. Apple מספקת שני מסלולים: מיגרציה מלאה (שבה SwiftData מנהל את הקובץ) ומצב hybrid (Core Data ו-SwiftData חולקים את אותו persistent store). לרוב האפליקציות אני ממליץ על גישה הדרגתית. תתחיל עם hybrid, ואז החלף מסך אחר מסך. עשיתי את הטעות של "רולאאוט אחד גדול" באפליקציה בינונית לפני שנה, ובילינו שבוע שלם בלרדוף אחר קרשים.

הטבלה מסכמת את ההבדלים המעשיים:

קריטריוןCore DataSwiftData
הגדרת מודלxcdatamodeld גרפימחלקת Swift עם @Model
type safety בשאילתותNSPredicate עם strings#Predicate DSL type-safe
אינטגרציה עם SwiftUI@FetchRequest, boilerplate רב@Query, שורה אחת
מיגרציה lightweightאוטומטית עם optionsאוטומטית לחלוטין
סנכרון CloudKitNSPersistentCloudKitContainercloudKitDatabase: .automatic
גיל מינימלי של iOSiOS 3.0iOS 17.0 (iOS 26 מומלץ)
Fetch asyncידני עם background contextמובנה עם ModelActor
קהילה ומאמריםגדולה, 15+ שניםצומחת, 2023 והלאה

לצורך מיגרציה בפועל, יש להגדיר את שני המודלים עם אותה URL של persistent store. הנה קטע קוד לפתיחת קובץ Core Data קיים דרך SwiftData:

let coreDataStoreURL = URL.applicationSupportDirectory
    .appending(path: "Model.sqlite")

let config = ModelConfiguration(
    schema: Schema([Book.self, Author.self]),
    url: coreDataStoreURL
)
let container = try ModelContainer(for: Schema([Book.self, Author.self]),
                                    configurations: [config])

לפני כן ודא ששמות ה-entities והשדות ב-xcdatamodeld זהים בדיוק לאלה של המודלים ב-SwiftData. כל אי-התאמה תזרוק Fatal error: Composite attribute.... אם השמות שונים, יש להעביר את הנתונים תוכנתית: קרוא מ-Core Data, יצור מודלים חדשים ב-SwiftData, ואז מחק את הקובץ הישן.

ביצועים, בדיקות ו-anti-patterns נפוצים

ל-SwiftData יש כמה מלכודות ביצועים שכדאי להכיר. הראשונה, וזו שאני רואה הכי הרבה בקוד ריוויו: אל תיצור ModelContainer חדש בכל init של SwiftUI View. זה מנתק את הזרם הריאקטיבי ויוצר לחץ מיותר על ה-CPU. תמיד יש להשתמש בקונטיינר יחיד שמוזרק דרך .modelContainer(container).

השנייה: פעולות כבדות (ייבוא, ניקוי) חייבות לרוץ ב-ModelActor ייעודי. הנה תבנית שאני משתמש בה בפרודקשן:

@ModelActor
actor BookImporter {
    func importBooks(from data: Data) throws {
        let decoded = try JSONDecoder().decode([BookDTO].self, from: data)
        for dto in decoded {
            let book = Book(
                isbn: dto.isbn,
                title: dto.title,
                author: dto.author,
                pageCount: dto.pageCount,
                publishedAt: dto.publishedAt
            )
            modelContext.insert(book)
        }
        try modelContext.save()
    }
}

// Usage from MainActor
Task {
    let importer = BookImporter(modelContainer: container)
    try await importer.importBooks(from: jsonData)
}

המקרו @ModelActor יוצר אוטומטית context מבודד ומאפשר לך לרוץ שאילתות ברקע בלי לחסום את המסך. הגישה הזו משתלבת יפה עם Swift Concurrency: TaskGroup ו-async let אם אתה מבצע ייבוא מקבילי מכמה API בו זמנית.

לבדיקות, השתמש ב-ModelConfiguration(isStoredInMemoryOnly: true) ליצירת container זמני. השילוב הזה עם מסגרת Swift Testing החדשה נותן לך suite של בדיקות מהירות שלא נוגעות בדיסק ולא דורשות ניקוי. גם מומלץ להשתמש ב-@MainActor בבדיקה בגלל שה-ModelContext של הסביבה מקובע ל-main thread.

ה-anti-pattern הנפוץ ביותר שאני רואה: שמירת [Book] ב-@State במקום להשתמש ב-@Query. זה מנתק אותך מעדכוני ה-context ואתה תתחיל לראות UI שלא מתעדכן. עוד anti-pattern: קריאה ל-context.save() אחרי כל insert. במקום זה, בצע batch של פעולות ואז save אחד. Apple מפרסמת את המלצות הביצועים הרשמיות ב-מדריך Preserving Your App's Model Data וכן ב-WWDC 2024 - What's New in SwiftData.

שאלות נפוצות

האם SwiftData מחליף לחלוטין את Core Data ב-iOS 26?

לרוב האפליקציות החדשות - כן. SwiftData מכסה 95% ממקרי השימוש של Core Data עם API פשוט יותר. עדיין נדרש Core Data ישירות עבור פעולות מתקדמות כמו NSBatchDeleteRequest על מיליוני רשומות, שאילתות async מוגבלות, או תמיכה במקורות נתונים שלא בבעלות האפליקציה.

האם ניתן להשתמש ב-SwiftData יחד עם Combine framework?

כן, אך זה לא מומלץ בפרויקטים חדשים. @Query מתעדכן דרך Observation framework החדש, לא דרך publishers של Combine. אם קוד legacy דורש publisher, יש לעטוף את השאילתה ידנית ב-Just או להשתמש ב-modelContext.willSave notification.

איך אני מגבה בסיס נתונים של SwiftData?

קובץ בסיס הנתונים ממוקם ב-Application Support וסיומתו .sqlite. יש להעתיק את שלושת הקבצים - .sqlite, .sqlite-wal, ו-.sqlite-shm - כיחידה אחת. פעולת העתקה חלקית תגרום לקובץ פגום. בפרודקשן אני משתמש ב-NSFileCoordinator כדי לוודא שאין כתיבה בזמן הגיבוי.

האם SwiftData תומך ב-full-text search?

לא באופן מובנה. עבור חיפוש טקסטואלי בסיסי, localizedStandardContains ב-#Predicate מספיק. עבור מנועי חיפוש מתקדמים, יש לשלב את SwiftData עם CoreSpotlight או להשתמש ב-SQLite FTS5 ישירות דרך CoreData underlying store.

מה קורה אם משתמש עדכן את האפליקציה מבלי סנכרון של iCloud?

SwiftData ישמור את הנתונים מקומית עד להצטרפות מחדש. אחרי חיבור ל-iCloud, הרשומות המקומיות יסונכרנו כשינויים חדשים. יש קונפליקט אפשרי אם אותה רשומה שונתה במכשיר אחר בזמן שהמשתמש היה offline - במקרה זה SwiftData מיישם last-write-wins על פי חתימת זמן.

אודות הכותב Priya Raghavan

Priya spent six years at Instacart building the iOS shopper app, where she led the migration from UIKit to SwiftUI across 80+ screens and cut crash-free sessions from 99.2% to 99.87%. Before that, she was a contractor at a Bay Area design studio shipping App Store apps for two Fortune 500 retail clients. She focuses on practical SwiftUI architecture - what holds up when you have 12 engineers committing to the same codebase, not just toy MVVM examples. Her recent work involves The Composable Architecture, Swift concurrency migration audits, and reducing main-thread hangs on older devices like the iPhone XR that enterprise fleets still ship. Priya runs a small consultancy in Oakland and occasionally speaks at try! Swift NYC. She has been writing Swift since the Objective-C bridging days of 2015.