swiftdata-patterns/SKILL.md
SwiftData patterns for iOS 17-26. Schema design, class inheritance (iOS 26), migrations, CloudKit sync, relationships, performance optimization, and background operations. The modern replacement for Core Data.
npx skillsauth add abanoub-ashraf/manus-skills-import swiftdata-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
SwiftData is Apple's modern persistence framework introduced in iOS 17, replacing Core Data with a Swift-native, declarative approach. iOS 26 adds class inheritance support for complex data hierarchies.
import SwiftData
@Model
class Task {
var title: String
var isComplete: Bool
var dueDate: Date?
var priority: Priority
var createdAt: Date
// Relationship
var project: Project?
init(title: String, priority: Priority = .medium) {
self.title = title
self.isComplete = false
self.priority = priority
self.createdAt = Date()
}
}
enum Priority: Int, Codable {
case low, medium, high, urgent
}
@Model
class Book {
var title: String
var author: String
// Index frequently queried properties for performance
@Attribute(.index)
var publicationYear: Int
init(title: String, author: String, publicationYear: Int) {
self.title = title
self.author = author
self.publicationYear = publicationYear
}
}
@Model
class User {
var name: String
// Enforce uniqueness - prevents duplicates
@Attribute(.unique)
var email: String
@Attribute(.unique)
var username: String
init(name: String, email: String, username: String) {
self.name = name
self.email = email
self.username = username
}
}
import SwiftData
// Base class with shared properties
@Model
class Event {
var title: String
var location: String
var scheduledDate: Date
var duration: TimeInterval
init(title: String, location: String, scheduledDate: Date, duration: TimeInterval) {
self.title = title
self.location = location
self.scheduledDate = scheduledDate
self.duration = duration
}
}
// Subclass extends base class
@Model
class Meeting: Event {
var attendees: [String]
var agenda: String
init(title: String, location: String, scheduledDate: Date, duration: TimeInterval, attendees: [String], agenda: String) {
self.attendees = attendees
self.agenda = agenda
super.init(title: title, location: location, scheduledDate: scheduledDate, duration: duration)
}
}
// Another subclass
@Model
class Workout: Event {
var exerciseType: String
var caloriesBurned: Int
init(title: String, location: String, scheduledDate: Date, duration: TimeInterval, exerciseType: String, caloriesBurned: Int) {
self.exerciseType = exerciseType
self.caloriesBurned = caloriesBurned
super.init(title: title, location: location, scheduledDate: scheduledDate, duration: duration)
}
}
// Query base class to get all events (meetings + workouts)
@Query var allEvents: [Event]
// Query specific subclass
@Query var meetings: [Meeting]
When to use inheritance:
Avoid inheritance when:
### Configure the Container
```swift
import SwiftUI
import SwiftData
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Task.self, Project.self])
}
}
// Or with custom configuration
@main
struct MyApp: App {
let container: ModelContainer
init() {
let schema = Schema([Task.self, Project.self])
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
allowsSave: true
)
do {
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
import SwiftUI
import SwiftData
struct TaskListView: View {
// Fetch all tasks
@Query var tasks: [Task]
var body: some View {
List(tasks) { task in
TaskRow(task: task)
}
}
}
struct TaskListView: View {
// With predicate and sort
@Query(
filter: #Predicate<Task> { !$0.isComplete },
sort: \.dueDate
)
var incompleteTasks: [Task]
// Multiple sort descriptors
@Query(
sort: [
SortDescriptor(\Task.priority, order: .reverse),
SortDescriptor(\Task.dueDate)
]
)
var sortedTasks: [Task]
// With animation
@Query(sort: \.createdAt, animation: .default)
var animatedTasks: [Task]
}
struct TaskListView: View {
@State private var showCompleted = false
@State private var searchText = ""
var body: some View {
TaskList(showCompleted: showCompleted, searchText: searchText)
}
}
struct TaskList: View {
@Query var tasks: [Task]
init(showCompleted: Bool, searchText: String) {
let predicate = #Predicate<Task> { task in
(showCompleted || !task.isComplete) &&
(searchText.isEmpty || task.title.localizedStandardContains(searchText))
}
_tasks = Query(filter: predicate, sort: \.createdAt)
}
var body: some View {
List(tasks) { task in
TaskRow(task: task)
}
}
}
struct AddTaskView: View {
@Environment(\.modelContext) private var context
@State private var title = ""
var body: some View {
Form {
TextField("Title", text: $title)
Button("Add Task") {
let task = Task(title: title)
context.insert(task)
// Auto-saves by default
}
}
}
}
struct TaskDetailView: View {
@Bindable var task: Task // Use @Bindable for two-way binding
var body: some View {
Form {
TextField("Title", text: $task.title)
Toggle("Complete", isOn: $task.isComplete)
DatePicker("Due Date", selection: Binding(
get: { task.dueDate ?? Date() },
set: { task.dueDate = $0 }
))
}
// Changes auto-save
}
}
struct TaskListView: View {
@Environment(\.modelContext) private var context
@Query var tasks: [Task]
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.onDelete(perform: deleteTasks)
}
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
context.delete(tasks[index])
}
}
}
func deleteAllCompletedTasks() throws {
try context.delete(model: Task.self, where: #Predicate { $0.isComplete })
}
func markAllAsComplete() throws {
let descriptor = FetchDescriptor<Task>(
predicate: #Predicate { !$0.isComplete }
)
let tasks = try context.fetch(descriptor)
for task in tasks {
task.isComplete = true
}
}
@Model
class Project {
var name: String
// One project has many tasks
@Relationship(deleteRule: .cascade, inverse: \Task.project)
var tasks: [Task] = []
init(name: String) {
self.name = name
}
}
@Model
class Task {
var title: String
// Many tasks belong to one project
var project: Project?
init(title: String) {
self.title = title
}
}
// Usage
let project = Project(name: "Work")
let task = Task(title: "Finish report")
task.project = project
// Or: project.tasks.append(task)
@Model
class Task {
var title: String
@Relationship(inverse: \Tag.tasks)
var tags: [Tag] = []
}
@Model
class Tag {
var name: String
var tasks: [Task] = []
init(name: String) {
self.name = name
}
}
// Usage
let task = Task(title: "Important work")
let urgentTag = Tag(name: "Urgent")
task.tags.append(urgentTag)
@Relationship(deleteRule: .cascade) // Delete children when parent deleted
@Relationship(deleteRule: .nullify) // Set relationship to nil (default)
@Relationship(deleteRule: .deny) // Prevent deletion if children exist
@Relationship(deleteRule: .noAction) // Do nothing (can orphan data)
SwiftData handles simple changes automatically:
@Attribute(originalName:))@Model
class Task {
var title: String
// Renamed property - SwiftData migrates automatically
@Attribute(originalName: "done")
var isComplete: Bool
// New property with default - no migration needed
var notes: String = ""
}
// Version 1
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[TaskV1.self]
}
@Model
class TaskV1 {
var title: String
var done: Bool
}
}
// Version 2
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self]
}
@Model
class Task {
var title: String
var isComplete: Bool
var priority: Int // New field
}
}
// Migration Plan
enum TaskMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SchemaV1.self, SchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self
) { context in
let tasks = try context.fetch(FetchDescriptor<SchemaV1.TaskV1>())
for task in tasks {
// Transform data as needed
}
try context.save()
}
}
// Use migration plan
let container = try ModelContainer(
for: Task.self,
migrationPlan: TaskMigrationPlan.self
)
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .automatic // or .private, .shared
)
let container = try ModelContainer(for: schema, configurations: config)
@Model
class Task {
var title: String
var isComplete: Bool
// Track modification for conflict resolution
var lastModified: Date = Date()
func update(title: String) {
self.title = title
self.lastModified = Date()
}
}
import CloudKit
class SyncMonitor: ObservableObject {
@Published var syncStatus: String = "Unknown"
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleCloudKitEvent),
name: NSPersistentCloudKitContainer.eventChangedNotification,
object: nil
)
}
@objc func handleCloudKitEvent(_ notification: Notification) {
if let event = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey]
as? NSPersistentCloudKitContainer.Event {
DispatchQueue.main.async {
switch event.type {
case .setup: self.syncStatus = "Setting up..."
case .import: self.syncStatus = "Importing..."
case .export: self.syncStatus = "Exporting..."
@unknown default: self.syncStatus = "Unknown"
}
if event.endDate != nil {
self.syncStatus = event.succeeded ? "Synced" : "Sync failed"
}
}
}
}
}
// Limit results
var descriptor = FetchDescriptor<Task>(
predicate: #Predicate { !$0.isComplete },
sortBy: [SortDescriptor(\.dueDate)]
)
descriptor.fetchLimit = 50
descriptor.fetchOffset = 0 // For pagination
let tasks = try context.fetch(descriptor)
var descriptor = FetchDescriptor<Project>()
descriptor.relationshipKeyPathsForPrefetching = [\Project.tasks]
let projects = try context.fetch(descriptor)
// tasks are already loaded, no additional fetches
// Perform heavy work on background
actor DataManager {
private let container: ModelContainer
init(container: ModelContainer) {
self.container = container
}
func importLargeDataset(_ items: [ImportItem]) async throws {
let context = ModelContext(container)
context.autosaveEnabled = false // Manual save for batch
for item in items {
let task = Task(title: item.title)
context.insert(task)
}
try context.save()
}
}
// Usage
let manager = DataManager(container: container)
Task.detached {
try await manager.importLargeDataset(items)
}
// Process large datasets without loading all into memory
func processAllTasks() throws {
var descriptor = FetchDescriptor<Task>()
descriptor.fetchLimit = 100
var offset = 0
var hasMore = true
while hasMore {
descriptor.fetchOffset = offset
let batch = try context.fetch(descriptor)
for task in batch {
processTask(task)
}
hasMore = batch.count == 100
offset += 100
// Reset context to free memory
context.reset()
}
}
import Testing
import SwiftData
@Suite struct TaskTests {
let container: ModelContainer
let context: ModelContext
init() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
container = try ModelContainer(for: Task.self, configurations: config)
context = ModelContext(container)
}
@Test func createTask() throws {
let task = Task(title: "Test Task")
context.insert(task)
let descriptor = FetchDescriptor<Task>()
let tasks = try context.fetch(descriptor)
#expect(tasks.count == 1)
#expect(tasks.first?.title == "Test Task")
}
@Test func deleteTask() throws {
let task = Task(title: "To Delete")
context.insert(task)
context.delete(task)
let tasks = try context.fetch(FetchDescriptor<Task>())
#expect(tasks.isEmpty)
}
}
@MainActor
protocol TaskRepository {
func all() throws -> [Task]
func incomplete() throws -> [Task]
func add(_ task: Task)
func delete(_ task: Task)
}
@MainActor
class SwiftDataTaskRepository: TaskRepository {
private let context: ModelContext
init(context: ModelContext) {
self.context = context
}
func all() throws -> [Task] {
try context.fetch(FetchDescriptor<Task>())
}
func incomplete() throws -> [Task] {
let predicate = #Predicate<Task> { !$0.isComplete }
return try context.fetch(FetchDescriptor(predicate: predicate))
}
func add(_ task: Task) {
context.insert(task)
}
func delete(_ task: Task) {
context.delete(task)
}
}
struct ContentView: View {
@Environment(\.modelContext) private var context
@Environment(\.undoManager) private var undoManager
var body: some View {
TaskListView()
.onAppear {
context.undoManager = undoManager
}
.toolbar {
Button("Undo") { undoManager?.undo() }
.disabled(!(undoManager?.canUndo ?? false))
Button("Redo") { undoManager?.redo() }
.disabled(!(undoManager?.canRedo ?? false))
}
}
}
Track changes to your data over time - useful for sync, undo, and audit trails.
// Fetch history transactions after a specific point
func fetchRecentChanges(after tokenData: Data) -> [DefaultHistoryTransaction] {
do {
// Decode the saved token
let token = try JSONDecoder().decode(DefaultHistoryToken.self, from: tokenData)
// Create history descriptor with predicate
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
descriptor.predicate = #Predicate { transaction in
transaction.token > token
}
// Fetch matching transactions
let transactions = try context.fetchHistory(descriptor)
return transactions
} catch {
return []
}
}
// Filter by author (e.g., changes from widget extension)
func fetchWidgetChanges(after token: DefaultHistoryToken) throws -> [DefaultHistoryTransaction] {
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
descriptor.predicate = #Predicate { transaction in
(transaction.token > token) && (transaction.author == "widget")
}
return try context.fetchHistory(descriptor)
}
// Track changes for sync
func getChangesSinceLastSync() -> [ModelChange] {
// Get all inserts, updates, deletes since last sync token
let transactions = fetchRecentChanges(after: lastSyncToken)
return transactions.flatMap { $0.changes }
}
Create custom storage backends (e.g., JSON files, remote databases).
import SwiftData
// Define custom store
final class JSONDataStore: DataStore {
typealias Configuration = JSONDataStoreConfiguration
typealias Snapshot = DefaultSnapshot
let configuration: Configuration
let identifier: String
init(_ configuration: Configuration, identifier: String) {
self.configuration = configuration
self.identifier = identifier
}
func fetch<T>(_ request: DataStoreFetchRequest<T>) throws -> DataStoreFetchResult<T, DefaultSnapshot> {
// Custom fetch implementation
}
func save(_ request: DataStoreSaveRequest<DefaultSnapshot>) throws -> DataStoreSaveResult<DefaultSnapshot> {
// Custom save implementation
}
}
// Use custom store
let config = JSONDataStoreConfiguration(url: fileURL)
let container = try ModelContainer(
for: Task.self,
configurations: ModelConfiguration(dataStore: JSONDataStore(config))
)
| Core Data | SwiftData |
|-----------|-----------|
| NSManagedObject | @Model class |
| NSPersistentContainer | ModelContainer |
| NSManagedObjectContext | ModelContext |
| NSFetchRequest | FetchDescriptor |
| NSPredicate | #Predicate |
| @FetchRequest | @Query |
| .xcdatamodeld | Swift code with @Model |
| — | @Attribute(.unique) (iOS 18) |
| — | @Attribute(.index) (iOS 18) |
| — | HistoryDescriptor (iOS 18) |
development
Design principles for building polished, native-feeling SwiftUI apps and widgets. Use this skill when creating or modifying SwiftUI views, iOS widgets (WidgetKit), or any native Apple UI. Ensures proper spacing, typography, colors, and widget implementations that look and feel like quality apps rather than AI-generated slop.
data-ai
Design and implement SwiftUI views, components, and app architecture. Use when creating new SwiftUI views, implementing MVVM/TCA patterns, managing state with @Observable, @State, @Binding, or @Environment, designing navigation flows, or structuring iOS app architecture. Triggers on SwiftUI, view model, state management, navigation, coordinator pattern.
development
Implement, review, or improve SwiftUI animations and transitions. Use when adding implicit or explicit animations with withAnimation, configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (bounce, pulse, variableColor, breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
testing
Audit SwiftUI views for accessibility (iOS + macOS) with patch-ready fixes