Skip to content
All essays
iOSMarch 27, 202513 min

SwiftData Complete Guide: Modern Data Persistence

Master SwiftData for data persistence in iOS apps. Models, queries, and relationships.

Ü
Ümit Uz
Mobile & Full Stack Developer

SwiftData is Apple's modern framework for data persistence, replacing Core Data with a simpler Swift-native API.

Getting Started with SwiftData

Basic Setup

swift
import SwiftData
import SwiftUI

@main
struct MyApp: App {
    let modelContainer: ModelContainer

    init() {
        do {
            modelContainer = try ModelContainer(
                for: Item.self
            )
        } catch {
            fatalError("Failed to create ModelContainer")
        }
    }

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

Defining Models

Simple Model

swift
import SwiftData
import Foundation

@Model
final class Item {
    var title: String
    var timestamp: Date
    var isCompleted: Bool

    init(title: String, timestamp: Date = .now, isCompleted: Bool = false) {
        self.title = title
        self.timestamp = timestamp
        self.isCompleted = isCompleted
    }
}

Relationships

swift
@Model
final class Author {
    var name: String
    @Relationship(deleteRule: .cascade) var books: [Book]

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

@Model
final class Book {
    var title: String
    var publicationYear: Int
    @Relationship(inverse: \Author.books) var author: Author?

    init(title: String, publicationYear: Int) {
        self.title = title
        self.publicationYear = publicationYear
    }
}

Advanced Model Configuration

swift
@Model
final class Product {
    var name: String
    var price: Double
    var quantity: Int

    // Unique identifier
    @Attribute(.unique) var uuid: UUID

    // Indexed for faster queries
    @Attribute(.unique) var sku: String

    // Exclude from persistent storage
    @Transient var temporaryValue: String = ""

    init(name: String, price: Double, quantity: Int) {
        self.name = name
        self.price = price
        self.quantity = quantity
        self.uuid = UUID()
        self.sku = "SKU-\(UUID().uuidString.prefix(8))"
    }
}

CRUD Operations

Create

swift
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        VStack {
            List {
                ForEach(items) { item in
                    Text(item.title)
                }
            }

            Button("Add Item") {
                addItem()
            }
        }
    }

    private func addItem() {
        let newItem = Item(title: "New Item")
        modelContext.insert(newItem)
    }
}

Read

swift
struct ContentView: View {
    // Basic query
    @Query private var items: [Item]

    // Filtered query
    @Query(filter: #Predicate<Item> { item in
        item.isCompleted == false
    }) var incompleteItems: [Item]

    // Sorted query
    @Query(sort: \Item.timestamp) var sortedItems: [Item]

    // Complex query
    @Query(
        filter: #Predicate<Item> { item in
            item.title.contains("important") &&
            item.isCompleted == false
        },
        sort: \Item.timestamp
    ) var importantItems: [Item]

    var body: some View {
        List {
            ForEach(items) { item in
                Text(item.title)
            }
        }
    }
}

Update

swift
struct EditView: View {
    @Environment(\.modelContext) private var modelContext
    var item: Item

    var body: some View {
        Form {
            TextField("Title", text: $item.title)
            Toggle("Completed", isOn: $item.isCompleted)
        }
        .onDisappear {
            // Changes are automatically saved
            try? modelContext.save()
        }
    }
}

Delete

swift
struct ItemRow: View {
    @Environment(\.modelContext) private var modelContext
    var item: Item

    var body: some View {
        HStack {
            Text(item.title)
            Spacer()
            Button("Delete") {
                modelContext.delete(item)
            }
        }
    }
}

Advanced Queries

Programmatic Queries

swift
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    private var modelContext: ModelContext

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

    func searchItems(_ searchText: String) {
        let predicate = #Predicate<Item> { item in
            item.title.contains(searchText)
        }

        let descriptor = FetchDescriptor<Item>(
            predicate: predicate,
            sortBy: [SortDescriptor(\Item.timestamp)]
        )

        do {
            items = try modelContext.fetch(descriptor)
        } catch {
            print("Search failed: \(error)")
        }
    }

    func findItemsCreatedAfter(_ date: Date) {
        let predicate = #Predicate<Item> { item in
            item.timestamp > date
        }

        let descriptor = FetchDescriptor<Item>(predicate: predicate)

        do {
            items = try modelContext.fetch(descriptor)
        } catch {
            print("Query failed: \(error)")
        }
    }
}

Data Migration

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

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

    static var schema: Schema {
        Schema(versionIdentifier) {
            // Define your models here
        }
    }
}

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [AppSchema.self]
    }

    static var stages: [MigrationStage] {
        []
    }

    static var migrationTypes: [any Migration.Type] {
        []
    }
}

SwiftData with SwiftUI

Real-time Updates

swift
struct LiveView: View {
    @Query private var items: [Item]

    var body: some View {
        List {
            ForEach(items) { item in
                HStack {
                    Text(item.title)
                    Spacer()
                    if item.isCompleted {
                        Image(systemName: "checkmark")
                    }
                }
            }
        }
        // Automatically updates when data changes
    }
}

Search and Filter

swift
struct SearchableView: View {
    @Query private var allItems: [Item]
    @State private var searchText = ""

    var filteredItems: [Item] {
        guard !searchText.isEmpty else { return allItems }
        return allItems.filter { $0.title.localizedStandardContains(searchText) }
    }

    var body: some View {
        List {
            ForEach(filteredItems) { item in
                Text(item.title)
            }
        }
        .searchable(text: $searchText)
    }
}

Best Practices

  1. 1Use models: Define clear data models
  2. 2Lazy loading: Use queries for large datasets
  3. 3Relationships: Define clear relationships
  4. 4Indexing: Add indexes for frequently queried properties
  5. 5Validation: Validate data before saving
  6. 6Migration: Plan for data model changes
  7. 7Testing: Test persistence layer thoroughly

SwiftData provides a modern, Swift-native approach to data persistence!

Related essays

Next essay
Network Programming in Swift: URLSession and Beyond