SwiftData

SwiftData is "a SwiftUI-optimized Core Data replacement" announced by Apple at WWDC23 in 2023, serving as a data persistence framework for Swift and SwiftUI. With the concept of "declarative and Swift-native data modeling," it provides intuitive model definition through @Model macros, automatic SwiftUI view updates via @Query, automatic CloudKit synchronization, and more. Leveraging Core Data's robust storage engine while delivering a modern Swift development experience, it represents Apple's next-generation data persistence solution.

Core DataSwiftiOSmacOSSwiftUIData persistenceApple

GitHub Overview

swiftlang/swift

The Swift Programming Language

Stars68,845
Watchers2,437
Forks10,499
Created:October 23, 2015
Language:C++
License:Apache License 2.0

Topics

None

Star History

swiftlang/swift Star History
Data as of: 7/19/2025, 08:07 AM

Library

SwiftData

Overview

SwiftData is "a SwiftUI-optimized Core Data replacement" announced by Apple at WWDC23 in 2023, serving as a data persistence framework for Swift and SwiftUI. With the concept of "declarative and Swift-native data modeling," it provides intuitive model definition through @Model macros, automatic SwiftUI view updates via @Query, automatic CloudKit synchronization, and more. Leveraging Core Data's robust storage engine while delivering a modern Swift development experience, it represents Apple's next-generation data persistence solution.

Details

SwiftData 2025 edition is positioned as the new standard for data persistence in the Apple ecosystem. Through macro-based declarative APIs (@Model, @Query, @Relationship), it achieves significantly more concise code writing compared to traditional Core Data. Deep integration with SwiftUI provides standard support for reactive programming where data changes automatically reflect in the UI. Supporting the latest platforms like iOS 17.0+, macOS 14.0+, it includes enterprise features such as CloudKit sync, migration, and backup/restore. However, over a year after its announcement, stability issues are still being reported, requiring careful consideration for production adoption.

Key Features

  • Declarative API: Intuitive data model definition through @Model macros
  • SwiftUI Integration: Automatic UI updates and reactive programming via @Query
  • CloudKit Sync: Automated cross-device data synchronization
  • Type Safety: Complete integration with Swift's type system
  • Migration: Automatic handling of schema changes
  • Core Data Compatibility: Leveraging Core Data's storage engine

Pros and Cons

Pros

  • Dramatically more concise code writing than Core Data, improving development efficiency
  • Deep integration with SwiftUI makes reactive UI implementation easy
  • Macro-based API significantly reduces boilerplate code
  • Automatic CloudKit sync simplifies multi-device support
  • Complete integration with Swift's type system ensures type safety
  • Leverages latest Swift language features (macros, property wrappers)

Cons

  • Only supports latest platforms: iOS 17+, macOS 14+
  • Over a year since announcement, still many reports of bugs and instability
  • Limited functionality compared to Core Data, cannot handle advanced requirements
  • Limited production track record raises reliability concerns
  • Difficult to implement complex data models or customizations
  • More limited debugging tools and documentation compared to Core Data

Reference Pages

Code Examples

Installation and Basic Setup

// iOS 17.0+, macOS 14.0+, watchOS 10.0+, tvOS 17.0+
import SwiftUI
import SwiftData

// Application setup
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [User.self, Post.self])
    }
}

// Model container with custom configuration
@main
struct MyApp: App {
    var container: ModelContainer
    
    init() {
        let schema = Schema([User.self, Post.self])
        let config = ModelConfiguration(
            schema: schema,
            isStoredInMemoryOnly: false,
            allowsSave: true,
            cloudKitDatabase: .automatic
        )
        
        do {
            container = try ModelContainer(for: schema, configurations: config)
        } catch {
            fatalError("Failed to configure SwiftData container: \(error)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container)
    }
}

Basic Model Definition

import SwiftData
import Foundation

// Basic model definition
@Model
class User {
    @Attribute(.unique) var id: UUID
    var name: String
    var email: String
    var age: Int
    var isActive: Bool
    var createdAt: Date
    var updatedAt: Date
    
    // Relationships
    @Relationship(deleteRule: .cascade, inverse: \Post.author)
    var posts: [Post] = []
    
    @Relationship(deleteRule: .nullify)
    var profile: UserProfile?
    
    init(name: String, email: String, age: Int) {
        self.id = UUID()
        self.name = name
        self.email = email
        self.age = age
        self.isActive = true
        self.createdAt = Date()
        self.updatedAt = Date()
    }
}

@Model
class Post {
    @Attribute(.unique) var id: UUID
    var title: String
    var content: String
    var isPublished: Bool
    var publishedAt: Date?
    var createdAt: Date
    var updatedAt: Date
    
    // Inverse relationship
    var author: User?
    
    @Relationship(deleteRule: .cascade, inverse: \Comment.post)
    var comments: [Comment] = []
    
    init(title: String, content: String, author: User) {
        self.id = UUID()
        self.title = title
        self.content = content
        self.isPublished = false
        self.createdAt = Date()
        self.updatedAt = Date()
        self.author = author
    }
    
    func publish() {
        isPublished = true
        publishedAt = Date()
        updatedAt = Date()
    }
}

@Model
class Comment {
    @Attribute(.unique) var id: UUID
    var content: String
    var createdAt: Date
    
    var post: Post?
    var author: User?
    
    init(content: String, post: Post, author: User) {
        self.id = UUID()
        self.content = content
        self.createdAt = Date()
        self.post = post
        self.author = author
    }
}

@Model
class UserProfile {
    @Attribute(.unique) var id: UUID
    var bio: String
    var website: String?
    var location: String?
    var avatar: Data?
    
    var user: User?
    
    init(bio: String, website: String? = nil, location: String? = nil) {
        self.id = UUID()
        self.bio = bio
        self.website = website
        self.location = location
    }
}

Data Operations in SwiftUI

import SwiftUI
import SwiftData

// Main view
struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var users: [User]
    @Query(sort: \Post.createdAt, order: .reverse) private var recentPosts: [Post]
    
    var body: some View {
        NavigationView {
            List {
                Section("Users") {
                    ForEach(users) { user in
                        NavigationLink(destination: UserDetailView(user: user)) {
                            UserRowView(user: user)
                        }
                    }
                    .onDelete(perform: deleteUsers)
                }
                
                Section("Recent Posts") {
                    ForEach(recentPosts.prefix(5)) { post in
                        PostRowView(post: post)
                    }
                }
            }
            .navigationTitle("SwiftData Demo")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    NavigationLink("Add User", destination: AddUserView())
                }
            }
        }
    }
    
    private func deleteUsers(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(users[index])
            }
        }
    }
}

// User row display
struct UserRowView: View {
    let user: User
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(user.name)
                .font(.headline)
            Text(user.email)
                .font(.caption)
                .foregroundColor(.secondary)
            HStack {
                Text("Age: \(user.age)")
                Spacer()
                Text("\(user.posts.count) posts")
                if user.isActive {
                    Image(systemName: "circle.fill")
                        .foregroundColor(.green)
                        .font(.caption)
                }
            }
            .font(.caption)
            .foregroundColor(.secondary)
        }
    }
}

// Add user view
struct AddUserView: View {
    @Environment(\.modelContext) private var modelContext
    @Environment(\.dismiss) private var dismiss
    
    @State private var name = ""
    @State private var email = ""
    @State private var age = 25
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $name)
                TextField("Email", text: $email)
                    .keyboardType(.emailAddress)
                Stepper("Age: \(age)", value: $age, in: 18...100)
            }
            .navigationTitle("Add User")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Save") {
                        saveUser()
                    }
                    .disabled(name.isEmpty || email.isEmpty)
                }
            }
        }
    }
    
    private func saveUser() {
        let newUser = User(name: name, email: email, age: age)
        modelContext.insert(newUser)
        
        do {
            try modelContext.save()
            dismiss()
        } catch {
            print("Failed to save user: \(error)")
        }
    }
}

Conditional Queries and Filtering

import SwiftUI
import SwiftData

// View using conditional queries
struct FilteredUsersView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var searchText = ""
    @State private var selectedAgeRange = 0
    @State private var showActiveOnly = false
    
    // Building dynamic queries
    private var filteredUsersQuery: Query<User, [User]> {
        var predicate = #Predicate<User> { user in
            true // Base condition
        }
        
        // Filtering by search text
        if !searchText.isEmpty {
            let searchPredicate = #Predicate<User> { user in
                user.name.localizedStandardContains(searchText) ||
                user.email.localizedStandardContains(searchText)
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && searchPredicate.evaluate(user)
            }
        }
        
        // Active users only filtering
        if showActiveOnly {
            let activePredicate = #Predicate<User> { user in
                user.isActive == true
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && activePredicate.evaluate(user)
            }
        }
        
        // Age range filtering
        switch selectedAgeRange {
        case 1: // 18-30
            let agePredicate = #Predicate<User> { user in
                user.age >= 18 && user.age <= 30
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && agePredicate.evaluate(user)
            }
        case 2: // 31-50
            let agePredicate = #Predicate<User> { user in
                user.age >= 31 && user.age <= 50
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && agePredicate.evaluate(user)
            }
        case 3: // 51+
            let agePredicate = #Predicate<User> { user in
                user.age >= 51
            }
            predicate = #Predicate<User> { user in
                predicate.evaluate(user) && agePredicate.evaluate(user)
            }
        default:
            break
        }
        
        return Query(filter: predicate, sort: \User.name)
    }
    
    var body: some View {
        VStack {
            // Filtering controls
            VStack(spacing: 16) {
                SearchBar(text: $searchText)
                
                Picker("Age Range", selection: $selectedAgeRange) {
                    Text("All Ages").tag(0)
                    Text("18-30").tag(1)
                    Text("31-50").tag(2)
                    Text("51+").tag(3)
                }
                .pickerStyle(SegmentedPickerStyle())
                
                Toggle("Active Users Only", isOn: $showActiveOnly)
            }
            .padding()
            
            // Filtered users list
            FilteredUsersList(query: filteredUsersQuery)
        }
        .navigationTitle("Filtered Users")
    }
}

// View displaying query results
struct FilteredUsersList: View {
    let query: Query<User, [User]>
    
    var body: some View {
        List(query.wrappedValue) { user in
            UserRowView(user: user)
        }
    }
}

// Search bar component
struct SearchBar: View {
    @Binding var text: String
    
    var body: some View {
        HStack {
            Image(systemName: "magnifyingglass")
                .foregroundColor(.secondary)
            
            TextField("Search users...", text: $text)
                .textFieldStyle(RoundedBorderTextFieldStyle())
            
            if !text.isEmpty {
                Button(action: {
                    text = ""
                }) {
                    Image(systemName: "xmark.circle.fill")
                        .foregroundColor(.secondary)
                }
            }
        }
    }
}

Data Migration and Versioning

import SwiftData

// Version 1 schema
enum SchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    
    static var models: [any PersistentModel.Type] {
        [UserV1.self, PostV1.self]
    }
    
    @Model
    class UserV1 {
        var name: String
        var email: String
        var age: Int
        
        init(name: String, email: String, age: Int) {
            self.name = name
            self.email = email
            self.age = age
        }
    }
    
    @Model
    class PostV1 {
        var title: String
        var content: String
        
        init(title: String, content: String) {
            self.title = title
            self.content = content
        }
    }
}

// Version 2 schema (adding new fields)
enum SchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    
    static var models: [any PersistentModel.Type] {
        [UserV2.self, PostV2.self]
    }
    
    @Model
    class UserV2 {
        @Attribute(.unique) var id: UUID
        var name: String
        var email: String
        var age: Int
        var isActive: Bool
        var createdAt: Date
        
        init(name: String, email: String, age: Int) {
            self.id = UUID()
            self.name = name
            self.email = email
            self.age = age
            self.isActive = true
            self.createdAt = Date()
        }
    }
    
    @Model
    class PostV2 {
        @Attribute(.unique) var id: UUID
        var title: String
        var content: String
        var isPublished: Bool
        var createdAt: Date
        
        init(title: String, content: String) {
            self.id = UUID()
            self.title = title
            self.content = content
            self.isPublished = false
            self.createdAt = Date()
        }
    }
}

// Migration plan definition
enum MigrationPlan: 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,
        willMigrate: { context in
            // Pre-migration processing
            print("Starting migration from V1 to V2")
        },
        didMigrate: { context in
            // Post-migration processing
            print("Completed migration from V1 to V2")
            
            // Setting default values
            let users = try context.fetch(FetchDescriptor<SchemaV2.UserV2>())
            for user in users {
                if user.id == UUID(uuidString: "00000000-0000-0000-0000-000000000000") {
                    user.id = UUID()
                }
                user.isActive = true
                user.createdAt = Date()
            }
            
            let posts = try context.fetch(FetchDescriptor<SchemaV2.PostV2>())
            for post in posts {
                if post.id == UUID(uuidString: "00000000-0000-0000-0000-000000000000") {
                    post.id = UUID()
                }
                post.isPublished = false
                post.createdAt = Date()
            }
            
            try context.save()
        }
    )
}

// Application setup with migration
@main
struct MigratableApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [SchemaV2.UserV2.self, SchemaV2.PostV2.self], migrationPlan: MigrationPlan.self)
    }
}

CloudKit Integration

import SwiftUI
import SwiftData
import CloudKit

// CloudKit integration setup
@main
struct CloudKitApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [User.self, Post.self]) { result in
            switch result {
            case .success(let container):
                // CloudKit configuration
                container.cloudKitDatabase = .automatic
                container.persistentStoreDescriptions.forEach { storeDescription in
                    storeDescription.setOption(true as NSNumber,
                                             forKey: NSPersistentHistoryTrackingKey)
                    storeDescription.setOption(true as NSNumber,
                                             forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
                }
            case .failure(let error):
                print("Failed to create container: \(error)")
            }
        }
    }
}

// CloudKit sync status monitoring
struct CloudKitSyncView: View {
    @Environment(\.modelContext) private var modelContext
    @State private var syncStatus: CloudKitSyncStatus = .unknown
    @State private var lastSyncDate: Date?
    
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            HStack {
                Text("Sync Status:")
                Spacer()
                StatusIndicator(status: syncStatus)
            }
            
            if let lastSync = lastSyncDate {
                HStack {
                    Text("Last Sync:")
                    Spacer()
                    Text(lastSync, style: .relative)
                        .foregroundColor(.secondary)
                }
            }
            
            Button("Force Sync") {
                forceSyncWithCloudKit()
            }
            .disabled(syncStatus == .syncing)
        }
        .padding()
        .background(Color.gray.opacity(0.1))
        .cornerRadius(8)
        .onAppear {
            monitorSyncStatus()
        }
    }
    
    private func forceSyncWithCloudKit() {
        syncStatus = .syncing
        
        // In SwiftData, CloudKit sync is automatic,
        // so manual force sync is usually unnecessary
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            syncStatus = .synced
            lastSyncDate = Date()
        }
    }
    
    private func monitorSyncStatus() {
        // CloudKit sync status monitoring implementation
        // In actual implementation, monitor NSPersistentCloudKitContainer events
    }
}

enum CloudKitSyncStatus {
    case unknown, syncing, synced, error
}

struct StatusIndicator: View {
    let status: CloudKitSyncStatus
    
    var body: some View {
        HStack {
            switch status {
            case .unknown:
                Image(systemName: "questionmark.circle")
                    .foregroundColor(.gray)
                Text("Unknown")
            case .syncing:
                ProgressView()
                    .scaleEffect(0.8)
                Text("Syncing...")
            case .synced:
                Image(systemName: "checkmark.circle.fill")
                    .foregroundColor(.green)
                Text("Synced")
            case .error:
                Image(systemName: "exclamationmark.triangle.fill")
                    .foregroundColor(.red)
                Text("Error")
            }
        }
        .font(.caption)
    }
}