Skip to content
intermediate Phase 6 · Data Persistence

SwiftData

Persist data with SwiftData: models, queries, relationships, migrations, and CloudKit sync.

1h
3 problems
Topic Progress 0%

Model Definition

What is SwiftData?

SwiftData is Apple's modern persistence framework introduced in iOS 17. It uses the @Model macro to define persistent objects that integrate seamlessly with SwiftUI.

Defining a Model

import SwiftData

@Model
class Article {
    var id: UUID
    var title: String
    var content: String
    var publishedAt: Date
    var isPublished: Bool

    init(title: String, content: String) {
        self.id = UUID()
        self.title = title
        self.content = content
        self.publishedAt = Date()
        self.isPublished = false
    }
}

The @Model macro generates persistence code automatically.

Model Container

Set up the container in your App:

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

Model Context

Access the context to save and fetch:

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

    func addArticle() {
        let article = Article(title: "New", content: "Hello")
        context.insert(article)
        try? context.save()
    }
}

Default Values

Provide defaults for model properties:

@Model
class Settings {
    var theme: String = "light"
    var notificationsEnabled: Bool = true
    var fontSize: Double = 16.0
}

Queries & Fetching

@Query Property Wrapper

@Query fetches data automatically and keeps it in sync:

struct ArticleListView: View {
    @Query(sort: \Article.publishedAt, order: .reverse)
    private var articles: [Article]

    var body: some View {
        List(articles) { article in
            Text(article.title)
        }
    }
}

Filtering Queries

@Query(filter: #Predicate<Article> { article in
    article.isPublished == true
}, sort: \Article.publishedAt)
private var publishedArticles: [Article]

Dynamic Queries

Use FetchDescriptor for dynamic filtering:

struct SearchView: View {
    @State private var searchText = ""
    @Query private var articles: [Article]

    var filteredArticles: [Article] {
        if searchText.isEmpty { return articles }
        return articles.filter { $0.title.localizedCaseInsensitiveContains(searchText) }
    }
}

FetchDescriptor

For complex queries:

let descriptor = FetchDescriptor<Article>(
    predicate: #Predicate { $0.isPublished == true },
    sortBy: [SortDescriptor(\Article.publishedAt, order: .reverse)]
)

let articles = try modelContext.fetch(descriptor)

Aggregate Queries

let count = try modelContext.fetchCount(descriptor)

Batch Operations

try modelContext.delete(model: Article.self, where: #Predicate { $0.isPublished == false })

Relationships & Migrations

One-to-Many Relationships

@Model
class Author {
    var name: String
    @Relationship(deleteRule: .cascade)
    var articles: [Article]

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

@Model
class Article {
    var title: String
    var author: Author?
}

Delete Rules

  • .nullify - Set relationship to nil (default)
  • .cascade - Delete related objects
  • .deny - Prevent deletion if related objects exist

Versioned Model Migration

enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Article.self]
    }
}

enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)

    static var models: [any PersistentModel.Type] {
        [Article.self]
    }

    static func migrate(_ migration: SchemaMigrationPlan) {
        migration.addSchema(Article.self)
    }
}

let container = try ModelContainer(
    for: Article.self,
    configurations: ModelConfiguration(schema: Schema([Article.self]))
)

Custom Migration

let migration = SchemaMigrationPlan("v1_to_v2")
migration.stage(fromVersion: SchemaV1.self, toVersion: SchemaV2.self) { context in
    // Custom migration logic
}

Quiz

1. What macro defines a SwiftData model?

Question 1 options

2. How do you access the model context in SwiftUI?

Question 2 options

3. What does @Query do in SwiftUI?

Question 3 options

4. What delete rule removes related objects?

Question 4 options

Flashcards

Question

What is SwiftData?

Answer

Apple's modern persistence framework (iOS 17+) using the @Model macro for automatic Swift object persistence.

Question

How do you set up a model container?

Answer

Use .modelContainer(for: Type.self) on the Scene in your App.

Question

What is the difference between @Query and FetchDescriptor?

Answer

@Query is a property wrapper for SwiftUI. FetchDescriptor is for imperative fetching in non-SwiftUI code.

Question

How do you filter queries in SwiftData?

Answer

Use #Predicate for type-safe filtering or filter: parameter in @Query.

Revision Notes

Key Takeaways

  • 1. @Model macro generates persistence code automatically for SwiftData models
  • 2. @Query provides automatic, synced data fetching in SwiftUI views
  • 3. @Environment(\.modelContext) is used for insert, save, and delete operations
  • 4. Relationships use @Relationship with delete rules (.cascade, .nullify, .deny)
  • 5. VersionedSchema enables safe data model migrations

Interview Tips

  • Explain how SwiftData simplifies persistence compared to Core Data
  • Know the difference between @Query and FetchDescriptor
  • Describe how to set up and use model containers
  • Discuss migration strategies with VersionedSchema

Cheat Sheet

@Model macro defines persistent models. @Query fetches data automatically. @Environment(.modelContext) for CRUD. .modelContainer(for:) on Scene. #Predicate for filtering. .cascade delete rule removes related objects. VersionedSchema for migrations.