Skip to main content

Working with SwiftData

1. Defining Models with @Model Macro

Property Type Description
@Model macro Defines a model class that can be used with SwiftData
@Attribute macro Defines a property as an attribute in the model
@Relationship macro Defines a property as a relationship to another model

Example: Basic Model Definition

import SwiftData

@Model
final class Book {
    var title: String
    var author: String
    var publishedDate: Date
    
    init(title: String, author: String, publishedDate: Date) {
        self.title = title
        self.author = author
        self.publishedDate = publishedDate
    }
}

Example: Model with Relationships

import SwiftData

@Model
final class Author {
    var name: String
    @Relationship(inverse: \.author)
    var books: [Book]
    
    init(name: String) {
        self.name = name
    }
}

@Model
final class Book {
    var title: String
    @Relationship(inverse: \.books)
    var author: Author?
    
    init(title: String) {
        self.title = title
    }
}

2. Creating Model Container

SwiftData uses a ModelContainer to manage the storage and retrieval of model objects. The container is initialized with a ModelSchema that defines the structure of your data models.

Property Type Description
ModelContainer struct Represents the storage container for SwiftData models.
ModelSchema struct Defines the schema of your data models.

Example: Creating a Model Container

import SwiftData

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [MyModel.self])
    }
}
Note: The modelContainer(for:) method is used to create a model container for the specified models. It automatically handles schema creation and migration.

3. Using ModelContext for Operations

Operation Description Code Example
insert(_:) Inserts a new entity into the context
let person = Person(name: "John", age: 30)
modelContext.insert(person)
delete(_:) Deletes an entity from the context modelContext.delete(person)
save() Saves all changes made in the context to the persistent store try modelContext.save()
fetch(_:) Fetches entities matching a specified predicate
let fetchRequest = FetchRequest(entity: Person.entity(), predicate: NSPredicate(format: "age > %d", 25))
let people = try modelContext.fetch(fetchRequest)
performFetch(_:) Performs a fetch operation asynchronously
let fetchRequest = FetchRequest(entity: Person.entity())
Task {
    let people = try await modelContext.performFetch(fetchRequest)
}
Note: ModelContext is a key part of SwiftData's API for managing data operations. It provides methods to insert, delete, and fetch entities.

4. Querying Data with @Query

Property Description Usage
@Query Declares a property that is automatically populated with query results from SwiftData @Query(variant: .count) var count: Int

Example: Basic @Query usage

import SwiftData

@Model
struct Book {
    var title: String
    var author: String
}

struct ContentView: View {
    @Query(
        .from(Book.self)
            .where(\.author == "J.K. Rowling")
    ) var books: [Book]

    var body: some View {
        List(books, id: \.title) { book in
            Text(book.title)
        }
    }
}

Example: @Query with sorting and limit

struct BookListView: View {
    @Query(
        .from(Book.self)
            .sorted(by: \.title, order: .ascending)
            .limit(10)
    ) var books: [Book]

    var body: some View {
        ForEach(books) { book in
            Text(book.title)
        }
    }
}

Example: @Query with count

struct BookCountView: View {
    @Query(
        variant: .count,
        .from(Book.self)
            .where(\.author == "George Orwell")
    ) var bookCount: Int

    var body: some View {
        Text("Books by George Orwell: \(bookCount)")
    }
}
Note: @Query is a property wrapper that automatically updates the view when data changes in SwiftData.
Warning: @Query does not support complex nested queries or subqueries. For advanced filtering, use fetch with SwiftData’s FetchDescriptor.

5. Inserting and Deleting Models

Operation Description Code Example
Inserting a Model Use insert method on a ModelContext to add a new model instance.
let context = ModelContext(modelContainer)
let newBook = Book(title: "iOS Development", author: "John Doe")
context.insert(newBook)
Deleting a Model Use delete method on a ModelContext to remove a model instance. context.delete(book)
Note: Changes to models must be committed using save on the ModelContext to persist changes to disk.

6. Working with Relationships

Relationship Type Description Code Example Notes
To-One Represents a single related object. @Relationship(inverse: \.user) var profile: Profile? Use optional for nullable relationships
To-Many Represents a collection of related objects. @Relationship(inverse: \.user) var posts: [Post] Defaults to empty array if no values
Inverse Relationship Defines the reverse side of a relationship. @Relationship(inverse: \.user) var profile: Profile? Must be defined on both sides for consistency

Example: Defining a To-Many Relationship

import SwiftData

@Model
final class User {
    var name: String
    @Relationship(inverse: \.user) var posts: [Post] = []
    
    init(name: String) {
        self.name = name
    }
}

@Model
final class Post {
    var title: String
    @Relationship(inverse: \.posts) var user: User?
    
    init(title: String) {
        self.title = title
    }
}
let context = try! ViewContext()
let user = try! context.fetch(FetchDescriptor<User>())
let posts = user.posts // Access related posts directly
Note: SwiftData automatically manages the lifecycle of related objects when they are fetched or modified.
Warning: Avoid circular references in relationships to prevent infinite loops during data loading.

7. Using Sort Descriptors

Property Description Usage
SortDescriptor Defines how to sort data in SwiftData SortDescriptor(\.name)
keyPath Property to sort by SortDescriptor(\.age)
ascending Sort direction (default true) SortDescriptor(\.name, ascending: false)

Example: Sorting with SortDescriptor

import SwiftData

struct Person: PersistentModel {
    var name: String
    var age: Int
}

let sortDescriptors = [
    SortDescriptor(\.age, ascending: false),
    SortDescriptor(\.name)
]

let results = try context.fetch(FetchDescriptor<Person>(sortDescriptors: sortDescriptors))

8. Filtering with Predicates

Property Type Description
predicate NSPredicate? Defines the condition to filter data

Example: Filtering with Predicate

import SwiftData

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

struct TaskListView: View {
    @Query(
        predicate: #Predicate<Task> { task in
            task.isCompleted == false
        }
    ) var tasks: [Task]
    
    var body: some View {
        List(tasks, id: \.self) { task in
            Text(task.title)
        }
    }
}
Note: Predicates in SwiftData are defined using the #Predicate macro for type-safe filtering.
Predicate Operator Description Example
== Equal to task.title == "Buy milk"
!= Not equal to task.isCompleted != true
< Less than task.priority < 3
> Greater than task.dueDate > Date()
contains Contains substring task.title.contains("grocery")
beginsWith Begins with string task.title.beginsWith("Important")
endsWith Ends with string task.title.endsWith("urgent")

Example: Complex Predicate

let predicate = #Predicate<Task> { task in
    task.isCompleted == false &&
    task.title.contains("grocery") &&
    task.dueDate <= Date().addingTimeInterval(86400) // within 24 hours
}

9. Migrating from Core Data

Core Data vs SwiftData Migration Considerations

  • Core Data is a mature framework with extensive documentation and community support, while SwiftData is newer and has less third-party tooling.
  • SwiftData uses a more modern, Swift-native approach with less boilerplate but lacks some of Core Data's advanced features like NSFetchedResultsController.
  • Migration strategies differ significantly; SwiftData provides built-in migration support, whereas Core Data requires more manual handling.
Feature Core Data SwiftData
Migration Strategy Manual implementation using NSMigrationManager or lightweight migration Built-in support for lightweight and custom migrations
Data Model Uses .xcdatamodeld files with XML-based schema definition Uses Swift structs and classes annotated with @Model
Performance Optimized for large datasets with background processing Designed for modern Swift performance, but may not handle very large datasets as efficiently
API Complex API with many classes (NSManagedObject, NSManagedObjectContext, etc.) Simpler API focused on Swift-native patterns

Example: Migrating a Simple Core Data Model to SwiftData

// Core Data Model
class User: NSManagedObject {
    @NSManaged var name: String
    @NSManaged var email: String
}

// SwiftData Model
import SwiftData

@Model
final class User {
    var name: String
    var email: String
    
    init(name: String, email: String) {
        self.name = name
        self.email = email
    }
}
Note: When migrating to SwiftData, ensure that all entities are properly annotated with @Model and that relationships are defined using Swift-native references rather than Core Data's NSManagedObject instances.
Warning: SwiftData does not support all Core Data features such as NSFetchedResultsController or complex predicate operations. Review your app's usage of these features before migration.

10. Understanding SwiftData vs Core Data

SwiftData vs Core Data

  • SwiftData is a modern, declarative framework introduced in iOS 17, designed for easier data persistence with Swift-first APIs. Core Data is a mature, robust framework that has been part of iOS/macOS for over 15 years, offering extensive features and backward compatibility.
  • SwiftData uses Swift-native syntax and is built on top of Core Data, simplifying development for new projects. Core Data offers more complex but powerful features like NSFetchedResultsController, custom NSManagedObject subclasses, and extensive mapping capabilities.
  • SwiftData is designed for modern Swift projects and supports SwiftUI integration out-of-the-box. Core Data requires more boilerplate and is more complex to integrate with SwiftUI.
  • SwiftData provides a simpler API for basic CRUD operations, while Core Data offers more granular control over data fetching and transformation.
  • SwiftData is not yet suitable for complex, large-scale applications or apps requiring extensive customization. Core Data is well-suited for large, complex applications with extensive data models and custom logic.
Feature SwiftData Core Data
API Style Declarative, Swift-native Traditional, Objective-C-based
Data Model Uses Swift structs and classes with @Model Uses NSManagedObject subclasses
SwiftUI Integration Native support with @FetchRequest Requires manual integration
Migration Handling Automatic migration in simple cases Manual migration handling with NSMigrationManager
Performance Optimized for modern Swift apps Highly optimized, but more complex to tune
Platform Support iOS 17+, macOS 14+ iOS 5+, macOS 10.4+

Was this article helpful?