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.
GitHub Overview
swiftlang/swift
The Swift Programming Language
Topics
Star History
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)
}
}