SwiftData: Ο Πλήρης Οδηγός για Αποθήκευση Δεδομένων σε iOS με Swift

Μάθετε SwiftData για iOS: από @Model και @Query μέχρι relationships, #Predicate, schema migrations, #Unique constraints, model inheritance (iOS 26) και history tracking. Με πρακτικά παραδείγματα κώδικα.

Εισαγωγή: Γιατί το SwiftData Αλλάζει τα Πάντα

Αν έχετε δουλέψει ποτέ με Core Data, ξέρετε ακριβώς το συναίσθημα. Αρχεία .xcdatamodeld, NSManagedObjectContext, NSFetchRequest, NSPredicate με string-based queries που ανακαλύπτεις ότι είναι λάθος μόνο στο runtime. Λειτουργούσε, ναι — αλλά ήταν verbose, error-prone, και ειλικρινά αισθανόταν ξεπερασμένο σε σχέση με τη μοντέρνα Swift.

Το SwiftData, που παρουσιάστηκε στο WWDC 2023, αλλάζει ριζικά αυτή την εικόνα. Είναι χτισμένο πάνω στο Core Data αλλά με ένα καθαρό Swift API: ορίζετε μοντέλα δεδομένων απευθείας στον κώδικα μέσω macros, κάνετε queries με type-safe predicates που ελέγχονται στο compile time, και τα ενσωματώνετε με SwiftUI σχεδόν χωρίς κόπο.

Λοιπόν, σε αυτόν τον οδηγό θα καλύψουμε κάθε πτυχή του SwiftData: από τη δημιουργία μοντέλων με το @Model macro μέχρι σύνθετα queries, relationships, schema migrations, #Unique constraints, model inheritance στο iOS 26, και history tracking. Κάθε ενότητα συνοδεύεται από πρακτικά παραδείγματα κώδικα που μπορείτε να χρησιμοποιήσετε αμέσως στα projects σας.

Τι Είναι το SwiftData και Γιατί να το Χρησιμοποιήσετε

Το SwiftData είναι το persistence framework της Apple, σχεδιασμένο εξαρχής για τη Swift. Αντικαθιστά — ή μάλλον, αναβαθμίζει — το Core Data, προσφέροντας:

  • Swift-native μοντελοποίηση: Ορίζετε τα μοντέλα σας ως κανονικές Swift classes με το @Model macro — χωρίς .xcdatamodeld αρχεία
  • Type-safe queries: Το #Predicate macro ελέγχεται στο compile time, εξαλείφοντας τα runtime crashes από λανθασμένα predicates
  • Αυτόματο iCloud sync: Ενσωματωμένος συγχρονισμός μέσω CloudKit χωρίς πρόσθετο κώδικα
  • Undo/Redo: Ενσωματωμένη υποστήριξη out of the box
  • SwiftUI integration: Το @Query macro ενημερώνει αυτόματα τα views όταν αλλάζουν τα δεδομένα
  • Lazy loading: Τα relationships φορτώνονται μόνο όταν πραγματικά τα χρειάζεστε

Υποστηρίζεται σε iOS 17+, macOS Sonoma+, tvOS 17+, watchOS 10+ και visionOS 1.0+, με σημαντικές βελτιώσεις σε κάθε νέα έκδοση.

Ρύθμιση Περιβάλλοντος

Για να ξεκινήσετε με το SwiftData, χρειάζεστε:

  • Xcode 26 (ή τουλάχιστον Xcode 15 για iOS 17)
  • Target iOS 17 ή νεότερο
  • Ένα απλό import:
import SwiftData

Ναι, αυτό είναι αρκετό. Κανένα αρχείο μοντέλου, κανένας data model editor, κανένας persistence controller. Απλά ένα import και είστε έτοιμοι.

Ορισμός Μοντέλων με το @Model Macro

Η καρδιά του SwiftData είναι το @Model macro. Παίρνει μια απλή Swift class και τη μετατρέπει σε persistent model:

import SwiftData

@Model
class Task {
    var title: String
    var notes: String
    var isCompleted: Bool
    var dueDate: Date?
    var priority: Int
    var createdAt: Date
    
    init(title: String, notes: String = "", isCompleted: Bool = false, dueDate: Date? = nil, priority: Int = 0) {
        self.title = title
        self.notes = notes
        self.isCompleted = isCompleted
        self.dueDate = dueDate
        self.priority = priority
        self.createdAt = .now
    }
}

Αυτό είναι! Η Swift δημιουργεί αυτόματα όλα τα απαραίτητα metadata για αποθήκευση στη βάση δεδομένων. Αν το συγκρίνετε με Core Data, εκεί θα χρειαζόσασταν ένα .xcdatamodeld αρχείο, μια generated NSManagedObject subclass, και αρκετά boilerplate. Η διαφορά είναι τεράστια.

Υποστηριζόμενοι Τύποι Δεδομένων

Το @Model υποστηρίζει εγγενώς αρκετούς τύπους:

  • String, Int, Double, Float, Bool
  • Date, Data, URL, UUID
  • Optionals όλων των παραπάνω
  • Codable structs και enums (αποθηκεύονται ως JSON)
  • Collections ([String], [Int], κ.λπ.)

Προσαρμογή Ιδιοτήτων με @Attribute

Αν θέλετε πιο λεπτομερή έλεγχο, μπορείτε να προσαρμόσετε τη συμπεριφορά μεμονωμένων ιδιοτήτων:

@Model
class Article {
    @Attribute(.unique) var slug: String
    @Attribute(.externalStorage) var imageData: Data?
    @Attribute(.spotlight) var title: String
    
    @Transient var cachedSummary: String? = nil
    
    init(slug: String, title: String, imageData: Data? = nil) {
        self.slug = slug
        self.title = title
        self.imageData = imageData
    }
}

Τα βασικά attribute options που θα χρησιμοποιείτε πιο συχνά:

  • .unique — Εξασφαλίζει μοναδικότητα της ιδιότητας
  • .externalStorage — Αποθηκεύει μεγάλα δεδομένα (π.χ. εικόνες) σε ξεχωριστά αρχεία αντί για inline στη βάση
  • .spotlight — Κάνει την ιδιότητα αναζητήσιμη μέσω Spotlight
  • @Transient — Η ιδιότητα δεν αποθηκεύεται στη βάση (χρήσιμο για cached ή computed τιμές)

ModelContainer και ModelContext: Ο Πυρήνας του Stack

Για να λειτουργήσει το SwiftData, χρειάζεστε δύο βασικά αντικείμενα. Ας τα δούμε ένα-ένα.

ModelContainer: Η Βάση Δεδομένων

Ο ModelContainer αντιπροσωπεύει τον αποθηκευτικό χώρο. Σε μια SwiftUI εφαρμογή, τον ρυθμίζετε στο entry point:

import SwiftUI
import SwiftData

@main
struct MyTaskApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Task.self)
    }
}

Αυτό δημιουργεί αυτόματα ένα SQLite αρχείο στο documents directory της εφαρμογής. Αν έχετε πολλά μοντέλα, απλά τα καταγράφετε μαζί:

.modelContainer(for: [Task.self, Category.self, Tag.self])

Προσαρμογή του Container

Για πιο σύνθετες ρυθμίσεις — π.χ. αν θέλετε CloudKit sync ή custom storage location — χρησιμοποιήστε το ModelConfiguration:

@main
struct MyTaskApp: App {
    let container: ModelContainer
    
    init() {
        let config = ModelConfiguration(
            "MyTasks",
            schema: Schema([Task.self, Category.self]),
            isStoredInMemoryOnly: false,
            cloudKitDatabase: .automatic
        )
        container = try! ModelContainer(
            for: Task.self, Category.self,
            configurations: config
        )
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

ModelContext: Η Ζωντανή Μνήμη

Ο ModelContext είναι η «ζωντανή» εκδοχή των δεδομένων σας — σκεφτείτε τον σαν ένα scratch pad. Οι αλλαγές γίνονται πρώτα στη μνήμη και αποθηκεύονται περιοδικά (ή χειροκίνητα) στη βάση. Στο SwiftUI, τον προσπελαύνετε μέσω environment:

struct TaskListView: View {
    @Environment(\.modelContext) private var modelContext
    
    // ...
}

Βασικές Λειτουργίες CRUD

Ας δούμε τις τέσσερις βασικές λειτουργίες. Θα εκπλαγείτε πόσο απλές είναι.

Create: Δημιουργία Νέων Αντικειμένων

func addTask(title: String, priority: Int) {
    let newTask = Task(title: title, priority: priority)
    modelContext.insert(newTask)
    // Το SwiftData αποθηκεύει αυτόματα —
    // δεν χρειάζεται ρητό save() στις περισσότερες περιπτώσεις
}

Read: Ανάγνωση Δεδομένων

Υπάρχουν δύο τρόποι ανάγνωσης: μέσω @Query στο SwiftUI (που θα δούμε αναλυτικά παρακάτω) ή μέσω FetchDescriptor στον κώδικα:

func fetchIncompleteTasks() throws -> [Task] {
    let descriptor = FetchDescriptor<Task>(
        predicate: #Predicate { !$0.isCompleted },
        sortBy: [SortDescriptor(\.dueDate)]
    )
    return try modelContext.fetch(descriptor)
}

Update: Ενημέρωση Δεδομένων

Εδώ είναι που το SwiftData λάμπει πραγματικά. Η ενημέρωση γίνεται απλά αλλάζοντας τις ιδιότητες — χωρίς save calls, χωρίς update methods:

func toggleTaskCompletion(_ task: Task) {
    task.isCompleted.toggle()
    // Οι αλλαγές αποθηκεύονται αυτόματα
}

Αυτό είναι. Σοβαρά.

Delete: Διαγραφή Δεδομένων

func deleteTask(_ task: Task) {
    modelContext.delete(task)
}

func deleteAllCompleted() throws {
    try modelContext.delete(
        model: Task.self,
        where: #Predicate { $0.isCompleted }
    )
}

Το @Query Macro: Δηλωτικά Queries στο SwiftUI

Το @Query macro είναι, κατά τη γνώμη μου, ένα από τα πιο ισχυρά χαρακτηριστικά του SwiftData. Φορτώνει αυτόματα δεδομένα και ενημερώνει το view κάθε φορά που αλλάζει κάτι — χωρίς manual refresh, χωρίς notifications:

struct TaskListView: View {
    @Query(sort: \Task.dueDate)
    private var tasks: [Task]
    
    var body: some View {
        List(tasks) { task in
            TaskRowView(task: task)
        }
    }
}

Φιλτράρισμα και Ταξινόμηση

// Μόνο ανολοκλήρωτες εργασίες, ταξινομημένες κατά προτεραιότητα
@Query(
    filter: #Predicate<Task> { !$0.isCompleted },
    sort: [
        SortDescriptor(\Task.priority, order: .reverse),
        SortDescriptor(\Task.dueDate)
    ]
)
private var pendingTasks: [Task]

Δυναμικά Queries

Τι γίνεται όμως όταν θέλετε ο χρήστης να φιλτράρει ή να ταξινομεί δυναμικά; Η λύση είναι λίγο πιο «τρικ»: μεταβιβάζετε τις παραμέτρους μέσω του initializer ενός child view:

struct FilteredTaskList: View {
    @Query private var tasks: [Task]
    
    init(searchText: String, showCompleted: Bool) {
        let predicate = #Predicate<Task> { task in
            (searchText.isEmpty || task.title.localizedStandardContains(searchText))
            && (showCompleted || !task.isCompleted)
        }
        _tasks = Query(
            filter: predicate,
            sort: [SortDescriptor(\Task.priority, order: .reverse)]
        )
    }
    
    var body: some View {
        List(tasks) { task in
            TaskRowView(task: task)
        }
    }
}

// Χρήση στο parent view
struct ContentView: View {
    @State private var searchText = ""
    @State private var showCompleted = false
    
    var body: some View {
        FilteredTaskList(
            searchText: searchText,
            showCompleted: showCompleted
        )
        .searchable(text: $searchText)
    }
}

Προσέξτε το _tasks — δεν αλλάζουμε τον πίνακα αποτελεσμάτων, αλλά ρυθμίζουμε το ίδιο το query. Επίσης, η χρήση localizedStandardContains αντί για contains εξασφαλίζει case-insensitive αναζήτηση — κάτι που είναι αναγκαίο αν η εφαρμογή σας υποστηρίζει ελληνικά κείμενα.

FetchDescriptor για Πιο Σύνθετα Queries

Αν χρειάζεστε fetch limit, offset ή πιο σύνθετη λογική, ο FetchDescriptor είναι ο φίλος σας:

// Τα 10 πιο πρόσφατα tasks
@Query(fetchDescriptor) private var recentTasks: [Task]

static var fetchDescriptor: FetchDescriptor<Task> {
    var descriptor = FetchDescriptor<Task>(
        sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
    )
    descriptor.fetchLimit = 10
    return descriptor
}

Relationships: Σχέσεις Μεταξύ Μοντέλων

Τα πραγματικά δεδομένα σπάνια υπάρχουν μεμονωμένα. Ένα task ανήκει σε μια κατηγορία, μια κατηγορία έχει πολλά tasks — αυτό ισχύει σε σχεδόν κάθε εφαρμογή. Ας δούμε πώς δουλεύουν οι σχέσεις στο SwiftData.

One-to-Many Σχέσεις

@Model
class Category {
    var name: String
    var colorHex: String
    
    @Relationship(deleteRule: .cascade, inverse: \Task.category)
    var tasks: [Task] = []
    
    init(name: String, colorHex: String = "#007AFF") {
        self.name = name
        self.colorHex = colorHex
    }
}

@Model
class Task {
    var title: String
    var isCompleted: Bool
    var category: Category?
    
    init(title: String, isCompleted: Bool = false, category: Category? = nil) {
        self.title = title
        self.isCompleted = isCompleted
        self.category = category
    }
}

Τα βασικά deleteRule που πρέπει να γνωρίζετε:

  • .nullify (προεπιλογή) — Αν διαγραφεί η Category, τα tasks αποκτούν category = nil
  • .cascade — Αν διαγραφεί η Category, διαγράφονται και τα tasks της (προσοχή με αυτό!)
  • .deny — Δεν επιτρέπεται η διαγραφή αν υπάρχουν σχετιζόμενα αντικείμενα

Many-to-Many Σχέσεις

@Model
class Tag {
    var name: String
    
    @Relationship(inverse: \Task.tags)
    var tasks: [Task] = []
    
    init(name: String) {
        self.name = name
    }
}

@Model
class Task {
    var title: String
    var tags: [Tag] = []
    
    init(title: String) {
        self.title = title
    }
}

// Χρήση
let urgentTag = Tag(name: "Επείγον")
let task = Task(title: "Παράδοση project")
task.tags.append(urgentTag)
modelContext.insert(task)

Ένα πράγμα που αξίζει να θυμάστε: τα relationships φορτώνονται lazy. Αν δεν χρησιμοποιήσετε ποτέ τα tasks μιας κατηγορίας, δεν θα γίνει ποτέ fetch. Αυτό εξοικονομεί μνήμη και χρόνο, ειδικά σε μεγάλα datasets.

Φιλτράρισμα μέσω Relationships

Μπορείτε να φιλτράρετε βάσει σχετιζόμενων αντικειμένων, κάτι που κάνει τα πράγματα πολύ πιο βολικά:

// Tasks που ανήκουν σε συγκεκριμένη κατηγορία
let categoryName = "Εργασία"
let predicate = #Predicate<Task> { task in
    task.category?.name == categoryName
}

// Tasks με συγκεκριμένο tag
let tagPredicate = #Predicate<Task> { task in
    task.tags.contains { $0.name == "Επείγον" }
}

Σύνθετα Predicates με το #Predicate Macro

Το #Predicate macro μετατρέπει Swift κώδικα σε database queries. Η μεγάλη διαφορά σε σχέση με τα string-based NSPredicate του Core Data; Τα σφάλματα εντοπίζονται στο compile time, όχι στο runtime με ένα μυστηριώδες crash:

// Σύνθετο predicate με πολλαπλά κριτήρια
let today = Date.now
let highPriority = 3

let predicate = #Predicate<Task> { task in
    !task.isCompleted
    && task.priority >= highPriority
    && (task.dueDate != nil && task.dueDate! <= today)
}

Σημαντική λεπτομέρεια: τα predicates εκτελούνται στο επίπεδο της βάσης δεδομένων, όχι στη μνήμη. Αυτό σημαίνει πολύ καλύτερη απόδοση σε σχέση με το να φορτώσετε τα πάντα και μετά να φιλτράρετε με .filter() στη Swift. Για λίγα αντικείμενα ίσως δεν φαίνεται η διαφορά, αλλά σε χιλιάδες εγγραφές είναι τεράστια.

Schema Migration: Εξέλιξη του Μοντέλου

Οι εφαρμογές εξελίσσονται, και μαζί τους τα μοντέλα δεδομένων. Αυτό είναι αναπόφευκτο. Το SwiftData παρέχει δύο τρόπους migration.

Lightweight Migration (Αυτόματη)

Για απλές αλλαγές — προσθήκη, αφαίρεση ή μετονομασία ιδιοτήτων — το SwiftData τα χειρίζεται μόνο του:

// Πριν (v1)
@Model
class Task {
    var title: String
    var isCompleted: Bool
}

// Μετά (v2) — προστέθηκε νέα ιδιότητα
@Model
class Task {
    var title: String
    var isCompleted: Bool
    var priority: Int = 0  // Νέα ιδιότητα με default τιμή
}

Εφόσον η νέα ιδιότητα έχει default τιμή, η migration γίνεται αυτόματα χωρίς απώλεια δεδομένων. Εύκολο, σωστά;

Custom Migration (Χειροκίνητη)

Για πιο σύνθετες αλλαγές — π.χ. αν θέλετε να μετατρέψετε ένα boolean σε string status — χρειάζεστε VersionedSchema και SchemaMigrationPlan:

enum TaskSchemaV1: VersionedSchema {
    static var versionIdentifier: Schema.Version = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] { [Task.self] }
    
    @Model
    class Task {
        var title: String
        var isCompleted: Bool
        init(title: String, isCompleted: Bool = false) {
            self.title = title
            self.isCompleted = isCompleted
        }
    }
}

enum TaskSchemaV2: VersionedSchema {
    static var versionIdentifier: Schema.Version = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] { [Task.self] }
    
    @Model
    class Task {
        var title: String
        var status: String  // Αντικατέστησε το isCompleted
        var priority: Int
        init(title: String, status: String = "pending", priority: Int = 0) {
            self.title = title
            self.status = status
            self.priority = priority
        }
    }
}

enum TaskMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [TaskSchemaV1.self, TaskSchemaV2.self]
    }
    
    static var stages: [MigrationStage] {
        [migrateV1toV2]
    }
    
    static let migrateV1toV2 = MigrationStage.custom(
        fromVersion: TaskSchemaV1.self,
        toVersion: TaskSchemaV2.self
    ) { context in
        // willMigrate: εκτελείται πριν τη migration
        let tasks = try context.fetch(FetchDescriptor<TaskSchemaV1.Task>())
        for task in tasks {
            // Μετατροπή bool σε string status
            // Η λογική εφαρμόζεται εδώ πριν αλλάξει το schema
        }
        try context.save()
    } didMigrate: { context in
        // didMigrate: εκτελείται μετά τη migration
    }
}

Compound Unique Constraints με #Unique (iOS 18+)

Από το iOS 18, μπορείτε να ορίσετε σύνθετους περιορισμούς μοναδικότητας με το #Unique macro. Αυτό είναι εξαιρετικά χρήσιμο αν εισάγετε δεδομένα από APIs ή εξωτερικές πηγές:

@Model
class Trip {
    #Unique<Trip>([\.name, \.startDate, \.endDate])
    
    var name: String
    var startDate: Date
    var endDate: Date
    var destination: String
    
    init(name: String, startDate: Date, endDate: Date, destination: String) {
        self.name = name
        self.startDate = startDate
        self.endDate = endDate
        self.destination = destination
    }
}

Με αυτό τον τρόπο, μπορείτε να έχετε πολλά ταξίδια με το ίδιο όνομα, αρκεί να έχουν διαφορετικές ημερομηνίες. Και το πιο ωραίο; Όταν εισάγετε ένα αντικείμενο που «συγκρούεται» με υπάρχον, το SwiftData εκτελεί upsert — ενημερώνει αντί να δημιουργεί διπλότυπο.

Βελτιστοποίηση Queries με #Index (iOS 18+)

Για μεγάλα datasets, τα ευρετήρια κάνουν τεράστια διαφορά στην ταχύτητα αναζήτησης:

@Model
class Task {
    #Index<Task>([\.dueDate], [\.priority], [\.isCompleted, \.dueDate])
    
    var title: String
    var isCompleted: Bool
    var dueDate: Date?
    var priority: Int
    
    init(title: String, isCompleted: Bool = false, dueDate: Date? = nil, priority: Int = 0) {
        self.title = title
        self.isCompleted = isCompleted
        self.dueDate = dueDate
        self.priority = priority
    }
}

Δημιουργήστε ευρετήρια στις ιδιότητες που χρησιμοποιείτε συχνά σε predicates και sort descriptors. Αλλά μην το παρακάνετε — κάθε ευρετήριο αυξάνει τον χρόνο εγγραφής, οπότε βάλτε μόνο εκεί που πραγματικά χρειάζεται.

Model Inheritance στο iOS 26

Αυτή είναι η μεγάλη καινοτομία του SwiftData στο iOS 26: υποστήριξη model inheritance. Για όσους έχουν ζητήσει αυτό το feature εδώ και χρόνια, επιτέλους ήρθε.

// Βασική κλάση
@Model
class Event {
    var title: String
    var date: Date
    var location: String?
    
    init(title: String, date: Date, location: String? = nil) {
        self.title = title
        self.date = date
        self.location = location
    }
}

// Υποκλάσεις
@Model
class WorkEvent: Event {
    var meetingLink: String?
    var attendees: [String] = []
    
    init(title: String, date: Date, meetingLink: String? = nil) {
        super.init(title: title, date: date)
        self.meetingLink = meetingLink
    }
}

@Model
class SocialEvent: Event {
    var dress_code: String?
    var isCasual: Bool = true
    
    init(title: String, date: Date, dressCode: String? = nil) {
        super.init(title: title, date: date)
        self.dress_code = dressCode
    }
}

Type-Based Querying

Το ωραίο με τα inheritance models είναι ότι μπορείτε να κάνετε query με βάση τον τύπο:

// Όλα τα events (βασικά + υποκλάσεις)
@Query var allEvents: [Event]

// Μόνο work events μέσω FetchDescriptor
let workEvents = try modelContext.fetch(
    FetchDescriptor<WorkEvent>()
)

// Φιλτράρισμα βάσει τύπου με predicate
let socialOnly = #Predicate<Event> { $0 is SocialEvent }

Η κληρονομικότητα λειτουργεί καλά όταν τα μοντέλα σχηματίζουν φυσικές ιεραρχίες — σκεφτείτε τη σχέση «is-a». Ένα WorkEvent είναι ένα Event. Αν η σχέση είναι «has-a» (δηλαδή ένα αντικείμενο περιέχει ένα άλλο), χρησιμοποιήστε relationships.

History Tracking: Παρακολούθηση Αλλαγών (iOS 18+)

Το SwiftData History σάς δίνει τη δυνατότητα να παρακολουθείτε τις αλλαγές στα δεδομένα σας. Αυτό είναι ιδιαίτερα χρήσιμο αν χρειάζεστε συγχρονισμό με server ή θέλετε να αντιδράτε σε αλλαγές από app extensions:

// Ανάκτηση transactions μετά από ένα token
func processRecentChanges(after token: DefaultHistoryToken?) throws {
    var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
    if let token {
        descriptor.predicate = #Predicate { $0.token > token }
    }
    
    let transactions = try modelContext.fetchHistory(descriptor)
    
    for transaction in transactions {
        for change in transaction.changes {
            switch change {
            case .insert(let inserted):
                print("Νέο αντικείμενο: \(inserted.changedPersistentIdentifier)")
            case .update(let updated):
                print("Ενημερώθηκε: \(updated.changedPersistentIdentifier)")
            case .delete(let deleted):
                print("Διαγράφηκε: \(deleted.changedPersistentIdentifier)")
            default:
                break
            }
        }
    }
}

Τα tokens λειτουργούν σαν σελιδοδείκτες: αποθηκεύετε το τελευταίο token που επεξεργαστήκατε και την επόμενη φορά ζητάτε μόνο τις νεότερες αλλαγές. Αρκετά έξυπνο σύστημα. Αν ένα token λήξει (γιατί διαγράφηκε μέρος του ιστορικού), θα λάβετε ένα historyTokenExpired error — οπότε φροντίστε να το χειρίζεστε στον κώδικά σας.

Χρήση SwiftData Εκτός SwiftUI

Αν χρειάζεστε SwiftData σε background tasks ή σε μη-SwiftUI κώδικα (κάτι πολύ συνηθισμένο σε πραγματικές εφαρμογές), χρησιμοποιήστε το @ModelActor:

@ModelActor
actor DataService {
    func importTasks(from jsonData: Data) throws {
        let decoded = try JSONDecoder().decode([TaskDTO].self, from: jsonData)
        for dto in decoded {
            let task = Task(
                title: dto.title,
                priority: dto.priority
            )
            modelContext.insert(task)
        }
        try modelContext.save()
    }
    
    func countIncompleteTasks() throws -> Int {
        let descriptor = FetchDescriptor<Task>(
            predicate: #Predicate { !$0.isCompleted }
        )
        return try modelContext.fetchCount(descriptor)
    }
}

Το @ModelActor δημιουργεί αυτόματα ένα ξεχωριστό ModelContext, εξασφαλίζοντας thread safety χωρίς να χρειάζεται να ανησυχείτε για concurrency issues. Αν έχετε δουλέψει με performBackgroundTask στο Core Data, θα εκτιμήσετε πόσο πιο απλό είναι αυτό.

Πρακτικό Παράδειγμα: Εφαρμογή Σημειώσεων

Ας βάλουμε τα πάντα μαζί σε ένα ολοκληρωμένο παράδειγμα — μια μικρή εφαρμογή σημειώσεων:

import SwiftUI
import SwiftData

// MARK: - Models

@Model
class Notebook {
    var name: String
    var colorHex: String
    var createdAt: Date
    
    @Relationship(deleteRule: .cascade, inverse: \Note.notebook)
    var notes: [Note] = []
    
    init(name: String, colorHex: String = "#007AFF") {
        self.name = name
        self.colorHex = colorHex
        self.createdAt = .now
    }
}

@Model
class Note {
    #Index<Note>([\.updatedAt], [\.title])
    
    var title: String
    var content: String
    var updatedAt: Date
    var isPinned: Bool
    var notebook: Notebook?
    
    init(title: String, content: String = "", notebook: Notebook? = nil) {
        self.title = title
        self.content = content
        self.updatedAt = .now
        self.isPinned = false
        self.notebook = notebook
    }
}

// MARK: - Views

struct NotesListView: View {
    @Query private var notes: [Note]
    @Environment(\.modelContext) private var modelContext
    
    init(searchText: String = "", notebook: Notebook? = nil) {
        let notebookName = notebook?.name
        _notes = Query(
            filter: #Predicate<Note> { note in
                (searchText.isEmpty || note.title.localizedStandardContains(searchText))
                && (notebookName == nil || note.notebook?.name == notebookName)
            },
            sort: [
                SortDescriptor(\Note.isPinned, order: .reverse),
                SortDescriptor(\Note.updatedAt, order: .reverse)
            ]
        )
    }
    
    var body: some View {
        List {
            ForEach(notes) { note in
                NoteRowView(note: note)
            }
            .onDelete(perform: deleteNotes)
        }
    }
    
    private func deleteNotes(at offsets: IndexSet) {
        for index in offsets {
            modelContext.delete(notes[index])
        }
    }
}

Βέλτιστες Πρακτικές και Συμβουλές

Μετά από αρκετή δουλειά με το SwiftData, ορισμένα πράγματα που αξίζει να θυμάστε:

  • Μην φορτώνετε τα πάντα: Χρησιμοποιήστε fetchLimit και fetchOffset στα FetchDescriptor σας για pagination — το UI σας θα σας ευχαριστήσει
  • Δημιουργήστε ευρετήρια στρατηγικά: Μόνο στις ιδιότητες που χρησιμοποιείτε σε predicates και sorts. Κάθε ευρετήριο κοστίζει σε εγγραφές
  • Χρησιμοποιήστε @ModelActor για background εργασίες: Μην τρέχετε βαριές λειτουργίες στο mainContext, θα κολλήσει το UI
  • Προτιμήστε predicates αντί .filter(): Τα predicates εκτελούνται στη βάση δεδομένων, ενώ το .filter() φορτώνει τα πάντα στη μνήμη πρώτα
  • Κρατήστε τα μοντέλα απλά: Τα Codable properties λειτουργούν, αλλά δεν μπορείτε πάντα να τα χρησιμοποιήσετε σε predicates
  • Δοκιμάστε τα migrations: Πριν κάνετε release, βεβαιωθείτε ότι η migration λειτουργεί σωστά με παλαιότερα δεδομένα. Αυτό γλιτώνει πολύ πονοκέφαλο
  • Χρησιμοποιήστε #Unique για αποφυγή διπλότυπων: Ειδικά αν εισάγετε δεδομένα από εξωτερικές πηγές ή APIs

Συχνές Ερωτήσεις (FAQ)

Μπορώ να χρησιμοποιήσω SwiftData μαζί με Core Data στην ίδια εφαρμογή;

Ναι, τα δύο frameworks μπορούν να συνυπάρχουν. Το SwiftData είναι χτισμένο πάνω στο Core Data και χρησιμοποιεί τον ίδιο μηχανισμό αποθήκευσης. Ωστόσο, πρέπει να προσέξετε τα μοντέλα σας να είναι ακριβώς τα ίδια και στα δύο — οποιαδήποτε ασυμφωνία μπορεί να προκαλέσει ανεπιθύμητη migration.

Χρειάζεται να καλέσω save() χειροκίνητα;

Στις περισσότερες περιπτώσεις, όχι. Το SwiftData αποθηκεύει αυτόματα μέσω του mainContext σε τακτά διαστήματα και όταν η εφαρμογή πηγαίνει σε background. Ωστόσο, σε background tasks με @ModelActor, πρέπει να καλέσετε try modelContext.save() ρητά — αλλιώς οι αλλαγές σας θα χαθούν.

Λειτουργεί το @Query εκτός SwiftUI views;

Όχι. Το @Query macro λειτουργεί μόνο μέσα σε SwiftUI views. Εκτός views, χρησιμοποιήστε FetchDescriptor με modelContext.fetch() ή modelContext.fetchCount().

Πώς διαχειρίζεται το SwiftData τον συγχρονισμό με iCloud;

Ο συγχρονισμός μέσω CloudKit ενεργοποιείται ρυθμίζοντας cloudKitDatabase: .automatic στο ModelConfiguration. Το SwiftData χειρίζεται αυτόματα τις συγκρούσεις με πολιτική last-writer-wins και τρέχει τον συγχρονισμό στο background.

Ποια είναι η μεγαλύτερη διαφορά μεταξύ SwiftData και Core Data;

Ειλικρινά, η κύρια διαφορά είναι η εμπειρία ανάπτυξης. Το SwiftData χρησιμοποιεί Swift macros (@Model, @Query, #Predicate) αντί για XML αρχεία μοντέλων και string-based predicates. Τα type-safe predicates πιάνουν τα σφάλματα στο compile time αντί για runtime crashes, και η ενσωμάτωση με SwiftUI είναι σημαντικά πιο απλή. Αν ξεκινάτε νέο project σήμερα, δεν υπάρχει λόγος να μην επιλέξετε SwiftData.

Σχετικά με τον Συγγραφέα Editorial Team

Our team of expert writers and editors.