swiftdata-persistence

SwiftData Persistence

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swiftdata-persistence" with this command: npx skills add bluewaves-creations/bluewaves-skills/bluewaves-creations-bluewaves-skills-swiftdata-persistence

SwiftData Persistence

Comprehensive guide to SwiftData framework, the @Model macro, reactive queries, relationships, and native iCloud synchronization for iOS 26 development.

Prerequisites

  • iOS 17+ for SwiftData (iOS 26 recommended)

  • Xcode 26+

@Model Macro Basics

Defining a Model

import SwiftData

@Model class Note { var title: String var content: String var createdAt: Date var isPinned: Bool

init(title: String, content: String = "") {
    self.title = title
    self.content = content
    self.createdAt = Date()
    self.isPinned = false
}

}

What @Model Provides

The @Model macro automatically:

  • Makes the class persistable

  • Tracks property changes

  • Enables SwiftUI observation

  • Generates schema metadata

Model Requirements

@Model class Item { // All stored properties must be: // - Codable types (String, Int, Date, Data, etc.) // - Other @Model types (relationships) // - Arrays/optionals of the above

var name: String           // ✓ Codable
var count: Int             // ✓ Codable
var timestamp: Date        // ✓ Codable
var data: Data             // ✓ Codable
var tags: [String]         // ✓ Array of Codable
var metadata: [String: String]  // ✓ Dictionary of Codable
var related: RelatedItem?  // ✓ Optional @Model relationship

// Computed properties are NOT persisted
var displayName: String {
    name.uppercased()
}

init(name: String) {
    self.name = name
    self.count = 0
    self.timestamp = Date()
    self.data = Data()
    self.tags = []
}

}

Model Attributes

@Attribute Macro

@Model class User { // Unique constraint (NOT compatible with iCloud sync) @Attribute(.unique) var email: String

// Spotlight indexing
@Attribute(.spotlight)
var name: String

// External storage for large data
@Attribute(.externalStorage)
var profileImage: Data?

// Encryption (device-only, not synced to iCloud)
@Attribute(.encrypt)
var sensitiveData: String?

// Preserve value when nil assigned
@Attribute(.preserveValueOnDeletion)
var archiveReason: String?

// Ephemeral (not persisted)
@Attribute(.ephemeral)
var temporaryState: String?

// Custom original name for migration
@Attribute(originalName: "userName")
var displayName: String

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

}

@Transient Macro

@Model class Document { var title: String var content: String

// Not persisted, recalculated
@Transient
var wordCount: Int = 0

init(title: String, content: String) {
    self.title = title
    self.content = content
    self.wordCount = content.split(separator: " ").count
}

}

Relationships

One-to-Many Relationship

@Model class Folder { var name: String

// One folder has many notes
@Relationship(deleteRule: .cascade)
var notes: [Note] = []

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

}

@Model class Note { var title: String var content: String

// Many notes belong to one folder (inverse)
var folder: Folder?

init(title: String, content: String = "", folder: Folder? = nil) {
    self.title = title
    self.content = content
    self.folder = folder
}

}

Many-to-Many Relationship

@Model class Note { var title: String

// Note can have many tags
@Relationship(inverse: \Tag.notes)
var tags: [Tag] = []

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

}

@Model class Tag { var name: String

// Tag can be on many notes
var notes: [Note] = []

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

}

Delete Rules

@Relationship(deleteRule: .cascade) // Delete related objects @Relationship(deleteRule: .nullify) // Set relationship to nil (default) @Relationship(deleteRule: .deny) // Prevent deletion if related exist @Relationship(deleteRule: .noAction) // Do nothing

iCloud-Compatible Relationships

Important: For iCloud sync, all relationships MUST be optional:

@Model class Note { var title: String

// REQUIRED for iCloud: Optional relationships
var folder: Folder?
var tags: [Tag]?  // Optional array

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

}

ModelContainer Configuration

Basic Setup

@main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Note.self, Folder.self, Tag.self]) } }

Custom Configuration

@main struct MyApp: App { let container: ModelContainer

init() {
    let schema = Schema([Note.self, Folder.self, Tag.self])

    let config = ModelConfiguration(
        schema: schema,
        isStoredInMemoryOnly: false,
        allowsSave: true
    )

    do {
        container = try ModelContainer(for: schema, configurations: config)
    } catch {
        fatalError("Failed to configure SwiftData: \(error)")
    }
}

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

}

Multiple Configurations

let userConfig = ModelConfiguration( "UserData", schema: Schema([User.self]), url: userDataURL )

let cacheConfig = ModelConfiguration( "Cache", schema: Schema([CachedItem.self]), isStoredInMemoryOnly: true )

let container = try ModelContainer( for: Schema([User.self, CachedItem.self]), configurations: userConfig, cacheConfig )

Native iCloud Sync

Enabling iCloud Sync (One Line!)

SwiftData includes native iCloud sync - no CloudKit code required:

@main struct MyApp: App { let container: ModelContainer

init() {
    let schema = Schema([Note.self, Tag.self])

    let config = ModelConfiguration(
        schema: schema,
        cloudKitDatabase: .automatic  // That's it!
    )

    do {
        container = try ModelContainer(for: schema, configurations: config)
    } catch {
        fatalError("Failed to configure SwiftData: \(error)")
    }
}

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

}

cloudKitDatabase Options

// Automatic iCloud sync (recommended) cloudKitDatabase: .automatic

// Specific CloudKit container cloudKitDatabase: .private("iCloud.com.yourcompany.yourapp")

// No iCloud sync (local only) cloudKitDatabase: .none

Xcode Setup for iCloud

  • Select your target in Xcode

  • Go to "Signing & Capabilities"

  • Click "+ Capability"

  • Add "iCloud"

  • Check "CloudKit"

  • Select or create a CloudKit container

  • Add "Background Modes" capability

  • Check "Remote notifications"

iCloud-Compatible Model Requirements

Critical rules for iCloud sync:

@Model class Note { // ✓ Default values for non-optional properties var title: String = "" var content: String = "" var createdAt: Date = Date()

// ✓ Optional relationships
var folder: Folder?
var tags: [Tag]?

// ✗ NO unique constraints (not supported by CloudKit)
// @Attribute(.unique) var id: String  // DON'T DO THIS

// ✗ NO deny delete rules
// @Relationship(deleteRule: .deny)    // DON'T DO THIS

init(title: String = "", content: String = "") {
    self.title = title
    self.content = content
    self.createdAt = Date()
}

}

Schema Migration for iCloud

After shipping to production:

// DO: // - Add new optional properties with defaults // - Add new optional relationships

// DON'T: // - Delete properties (data loss) // - Rename properties (treated as delete + add) // - Change property types // - Add required properties without defaults

Initialize CloudKit Schema

Before first production release:

#if DEBUG // Run once to create CloudKit schema try container.mainContext.initializeCloudKitSchema() #endif

@Query Macro

Basic Query

struct NotesListView: View { @Query var notes: [Note]

var body: some View {
    List(notes) { note in
        Text(note.title)
    }
}

}

Sorted Query

// Single sort @Query(sort: \Note.createdAt, order: .reverse) var notes: [Note]

// Multiple sorts @Query(sort: [ SortDescriptor(\Note.isPinned, order: .reverse), SortDescriptor(\Note.createdAt, order: .reverse) ]) var notes: [Note]

Filtered Query

// Static predicate @Query(filter: #Predicate<Note> { note in note.isPinned == true }) var pinnedNotes: [Note]

// Complex predicate @Query(filter: #Predicate<Note> { note in note.title.contains("Swift") && !note.content.isEmpty }) var swiftNotes: [Note]

Dynamic Filtering

struct SearchableNotesView: View { @State private var searchText = ""

var body: some View {
    FilteredNotesView(searchText: searchText)
        .searchable(text: $searchText)
}

}

struct FilteredNotesView: View { @Query var notes: [Note]

init(searchText: String) {
    let predicate = #Predicate&#x3C;Note> { note in
        searchText.isEmpty || note.title.localizedStandardContains(searchText)
    }
    _notes = Query(filter: predicate, sort: \.createdAt, order: .reverse)
}

var body: some View {
    List(notes) { note in
        Text(note.title)
    }
}

}

Query with Limit

@Query(sort: \Note.createdAt, order: .reverse) var recentNotes: [Note]

// In view, limit manually List(recentNotes.prefix(10)) { note in Text(note.title) }

Query Animations

@Query(sort: \Note.title, animation: .default) var notes: [Note]

ModelContext Operations

Accessing Context

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

// ...

}

Creating Objects

func createNote() { let note = Note(title: "New Note") modelContext.insert(note) // Auto-saved on SwiftUI lifecycle events }

Explicit Save

func saveChanges() { do { try modelContext.save() } catch { print("Save failed: (error)") } }

Deleting Objects

func deleteNote(_ note: Note) { modelContext.delete(note) }

func deleteNotes(at offsets: IndexSet) { for index in offsets { modelContext.delete(notes[index]) } }

Fetching with Descriptor

func fetchRecentNotes() throws -> [Note] { let descriptor = FetchDescriptor<Note>( predicate: #Predicate { $0.isPinned }, sortBy: [SortDescriptor(.createdAt, order: .reverse)] ) return try modelContext.fetch(descriptor) }

// With limit func fetchTopNotes(limit: Int) throws -> [Note] { var descriptor = FetchDescriptor<Note>( sortBy: [SortDescriptor(.createdAt, order: .reverse)] ) descriptor.fetchLimit = limit return try modelContext.fetch(descriptor) }

Batch Operations

// Delete all matching predicate try modelContext.delete(model: Note.self, where: #Predicate { note in note.createdAt < cutoffDate })

// Enumerate for batch processing let descriptor = FetchDescriptor<Note>() try modelContext.enumerate(descriptor) { note in note.processedAt = Date() }

#Predicate Macro

Basic Predicates

// Equality #Predicate<Note> { $0.isPinned == true }

// Comparison #Predicate<Note> { $0.createdAt > someDate }

// String contains #Predicate<Note> { $0.title.contains("Swift") }

// Case-insensitive contains #Predicate<Note> { $0.title.localizedStandardContains(searchText) }

Compound Predicates

// AND #Predicate<Note> { note in note.isPinned && note.title.contains("Important") }

// OR #Predicate<Note> { note in note.isPinned || note.folder?.name == "Favorites" }

// NOT #Predicate<Note> { note in !note.content.isEmpty }

Optional Handling

#Predicate<Note> { note in note.folder?.name == "Work" }

// Check for nil #Predicate<Note> { note in note.folder != nil }

Array Predicates

// Array contains #Predicate<Note> { note in note.tags?.contains(where: { $0.name == "Important" }) ?? false }

// Array is empty #Predicate<Note> { note in note.tags?.isEmpty ?? true }

Model Inheritance (iOS 26)

Base and Derived Models

@Model class MediaItem { var title: String var createdAt: Date

init(title: String) {
    self.title = title
    self.createdAt = Date()
}

}

@Model final class Photo: MediaItem { var imageData: Data? var resolution: String?

init(title: String, imageData: Data?) {
    super.init(title: title)
    self.imageData = imageData
}

}

@Model final class Video: MediaItem { var duration: TimeInterval var thumbnailData: Data?

init(title: String, duration: TimeInterval) {
    super.init(title: title)
    self.duration = duration
}

}

Polymorphic Queries

// Query all media items (photos and videos) @Query var allMedia: [MediaItem]

// Query only photos @Query var photos: [Photo]

Background Operations

Background Context

func importData() async { let container = modelContainer

await Task.detached {
    let context = ModelContext(container)

    // Perform operations
    for item in largeDataSet {
        let note = Note(title: item.title)
        context.insert(note)
    }

    try? context.save()
}.value

}

Actor Isolation

@ModelActor actor DataImporter { func importNotes(from data: [ImportData]) throws { for item in data { let note = Note(title: item.title, content: item.content) modelContext.insert(note) } try modelContext.save() } }

// Usage let importer = DataImporter(modelContainer: container) try await importer.importNotes(from: importData)

Migration

Lightweight Migration (Automatic)

SwiftData handles lightweight migrations automatically:

  • Adding new properties with defaults

  • Removing properties

  • Adding optional relationships

Custom Migration

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

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

@Model
class Note {
    var title: String
    var content: String

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

}

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

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

@Model
class Note {
    var title: String
    var content: String
    var createdAt: Date  // New property

    init(title: String, content: String) {
        self.title = title
        self.content = content
        self.createdAt = Date()
    }
}

}

enum MyMigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [MySchemaV1.self, MySchemaV2.self] }

static var stages: [MigrationStage] {
    [migrateV1toV2]
}

static let migrateV1toV2 = MigrationStage.lightweight(
    fromVersion: MySchemaV1.self,
    toVersion: MySchemaV2.self
)

}

// Use in container let container = try ModelContainer( for: Note.self, migrationPlan: MyMigrationPlan.self )

Testing

In-Memory Testing

import Testing import SwiftData

@Test func testNoteCreation() throws { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try ModelContainer(for: Note.self, configurations: config) let context = ModelContext(container)

let note = Note(title: "Test", content: "Content")
context.insert(note)

let descriptor = FetchDescriptor&#x3C;Note>()
let notes = try context.fetch(descriptor)

#expect(notes.count == 1)
#expect(notes.first?.title == "Test")

}

SwiftUI Preview with Sample Data

@MainActor let previewContainer: ModelContainer = { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: Note.self, configurations: config)

// Insert sample data
let context = container.mainContext
let sampleNotes = [
    Note(title: "First Note", content: "Content 1"),
    Note(title: "Second Note", content: "Content 2")
]
sampleNotes.forEach { context.insert($0) }

return container

}()

#Preview { NotesListView() .modelContainer(previewContainer) }

Best Practices

  1. Design for iCloud from the Start

// GOOD: iCloud-compatible model @Model class Note { var title: String = "" var content: String = "" var folder: Folder? // Optional relationship var tags: [Tag]? // Optional array

init(title: String = "") {
    self.title = title
}

}

// AVOID: iCloud-incompatible @Model class Note { @Attribute(.unique) var id: String // Not supported var folder: Folder // Non-optional relationship }

  1. Use @Query for Reactive Data

// GOOD: Reactive updates @Query(sort: \Note.createdAt) var notes: [Note]

// AVOID: Manual fetching in views @State private var notes: [Note] = [] func loadNotes() { notes = try? context.fetch(FetchDescriptor<Note>()) }

  1. Explicit Save for Critical Data

func saveImportantChange() { modelContext.insert(criticalData) do { try modelContext.save() } catch { // Handle error appropriately } }

  1. Use Background Contexts for Heavy Work

func importLargeDataset() async { await Task.detached { let context = ModelContext(container) // Heavy operations try? context.save() }.value }

Official Resources

  • SwiftData Documentation

  • Syncing model data across devices

  • WWDC23: Meet SwiftData

  • WWDC23: Model your schema with SwiftData

  • WWDC24: What's new in SwiftData

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

photographer-testino

No summary provided by upstream source.

Repository SourceNeeds Review
General

photographer-lindbergh

No summary provided by upstream source.

Repository SourceNeeds Review
General

photographer-lachapelle

No summary provided by upstream source.

Repository SourceNeeds Review
General

photographer-vonunwerth

No summary provided by upstream source.

Repository SourceNeeds Review