SwiftData framework with @Model macro, @Query, relationships, and NATIVE iCloud sync. Use when user asks about data persistence, SwiftData, @Model, @Query, database storage, iCloud sync, or CoreData migration.
/plugin marketplace add bluewaves-creations/bluewaves-skills/plugin install swift-apple-dev@bluewaves-skillsThis skill is limited to using the following tools:
Comprehensive guide to SwiftData framework, the @Model macro, reactive queries, relationships, and native iCloud synchronization for iOS 26 development.
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
}
}
The @Model macro automatically:
@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
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
}
}
@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
}
}
@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
}
}
@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
}
}
@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
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
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Note.self, Folder.self, Tag.self])
}
}
@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)
}
}
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
)
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)
}
}
// Automatic iCloud sync (recommended)
cloudKitDatabase: .automatic
// Specific CloudKit container
cloudKitDatabase: .private("iCloud.com.yourcompany.yourapp")
// No iCloud sync (local only)
cloudKitDatabase: .none
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()
}
}
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
Before first production release:
#if DEBUG
// Run once to create CloudKit schema
try container.mainContext.initializeCloudKitSchema()
#endif
struct NotesListView: View {
@Query var notes: [Note]
var body: some View {
List(notes) { note in
Text(note.title)
}
}
}
// 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]
// 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]
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<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(sort: \Note.createdAt, order: .reverse)
var recentNotes: [Note]
// In view, limit manually
List(recentNotes.prefix(10)) { note in
Text(note.title)
}
@Query(sort: \Note.title, animation: .default)
var notes: [Note]
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// ...
}
func createNote() {
let note = Note(title: "New Note")
modelContext.insert(note)
// Auto-saved on SwiftUI lifecycle events
}
func saveChanges() {
do {
try modelContext.save()
} catch {
print("Save failed: \(error)")
}
}
func deleteNote(_ note: Note) {
modelContext.delete(note)
}
func deleteNotes(at offsets: IndexSet) {
for index in offsets {
modelContext.delete(notes[index])
}
}
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)
}
// 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()
}
// 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) }
// 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
}
#Predicate<Note> { note in
note.folder?.name == "Work"
}
// Check for nil
#Predicate<Note> { note in
note.folder != nil
}
// 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
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
}
}
// Query all media items (photos and videos)
@Query var allMedia: [MediaItem]
// Query only photos
@Query var photos: [Photo]
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
}
@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)
SwiftData handles lightweight migrations automatically:
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
)
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<Note>()
let notes = try context.fetch(descriptor)
#expect(notes.count == 1)
#expect(notes.first?.title == "Test")
}
@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)
}
// 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
}
// 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>())
}
func saveImportantChange() {
modelContext.insert(criticalData)
do {
try modelContext.save()
} catch {
// Handle error appropriately
}
}
func importLargeDataset() async {
await Task.detached {
let context = ModelContext(container)
// Heavy operations
try? context.save()
}.value
}
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.