axiom-swiftdata by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftdataApple 的原生持久化框架,使用 @Model 类和声明式查询。构建于 Core Data 之上,专为 SwiftUI 设计。
核心原则 引用类型(class)+ @Model 宏 + 声明式 @Query 以实现响应式 SwiftUI 集成。
要求 iOS 17+,Swift 5.9+ 目标 iOS 26+(本技能侧重于最新功能) 许可证 专有(Apple)
@Query 自动更新 UI广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关于迁移 如需使用 VersionedSchema 和 SchemaMigrationPlan 进行自定义模式迁移,请参阅 axiom-swiftdata-migration 技能。如需迁移调试,请参阅 axiom-swiftdata-migration-diag。
以下是开发者提出的真实问题,本技能旨在解答:
→ 本技能展示如何使用带有谓词、排序和自动视图更新的 @Query
→ 本技能解释带有 deleteRule: .cascade 的 @Relationship 和反向关系
→ 本技能展示级联删除、反向关系和安全删除模式
→ 本技能涵盖 CloudKit 集成、冲突解决策略(最后写入获胜、自定义解决)和同步模式
→ 本技能解释 CloudKit 约束:所有属性必须是可选的或具有默认值,解释原因(网络时序),并展示修复方法
→ 本技能展示如何使用通知监控同步状态、检测网络连接性以及离线感知的 UI 模式
→ 本技能涵盖 CloudKit 记录共享模式(iOS 26+),包括所有者/权限跟踪和共享元数据
→ 本技能涵盖性能模式、批量获取、限制查询以及通过分块导入防止内存膨胀
→ 本技能展示如何在没有预取的情况下识别 N+1 问题,提供预取模式,并展示 100 倍的性能提升
→ 本技能展示基于分块的导入、定期保存、内存清理模式和批量操作优化
→ 本技能解释索引优化模式:何时添加索引(频繁筛选/排序的属性),何时避免(很少使用、频繁更改的属性),维护成本
→ 请参阅下方迁移部分的对比表,然后按照 realm-to-swiftdata-migration 或 axiom-swiftdata-migration 获取详细指南
import SwiftData
@Model
final class Track {
@Attribute(.unique) var id: String
var title: String
var artist: String
var duration: TimeInterval
var genre: String?
init(id: String, title: String, artist: String, duration: TimeInterval, genre: String? = nil) {
self.id = id
self.title = title
self.artist = artist
self.duration = duration
self.genre = genre
}
}
final class,而不是 struct(如果需要子类,请省略 final — 参见下面的类继承部分)@Attribute(.unique) 实现类似主键的行为init(SwiftData 不会自动合成)String?)可为空@Attribute(.preserveValueOnDeletion)(适用于分析、审计追踪)@Model
final class Track {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album?
init(id: String, title: String, album: Album? = nil) {
self.id = id
self.title = title
self.album = album
}
}
@Model
final class Album {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade)
var tracks: [Track] = []
init(id: String, title: String) {
self.id = id
self.title = title
}
}
@MainActor // Swift 6 严格并发性要求
@Model
final class User {
@Attribute(.unique) var id: String
var name: String
// 关注此用户的用户(反向关系)
@Relationship(deleteRule: .nullify, inverse: \User.following)
var followers: [User] = []
// 此用户关注的用户
@Relationship(deleteRule: .nullify)
var following: [User] = []
init(id: String, name: String) {
self.id = id
self.name = name
}
}
✅ 正确 — 只修改一侧
// user1 关注 user2(修改一侧)
user1.following.append(user2)
try modelContext.save()
// SwiftData 自动更新 user2.followers
// 不要手动向两侧都添加 - 会导致重复!
❌ 错误 — 不要手动更新两侧
user1.following.append(user2)
user2.followers.append(user1) // 冗余!在 CloudKit 同步中创建重复项
user1.following.removeAll { $0.id == user2.id }
try modelContext.save()
// user2.followers 自动更新
// 检查关系是否真正是双向的
let user1FollowsUser2 = user1.following.contains { $0.id == user2.id }
let user2FollowedByUser1 = user2.followers.contains { $0.id == user1.id }
// 这些在 save() 后必须始终匹配
assert(user1FollowsUser2 == user2FollowedByUser1, "关系损坏!")
// 如果 CloudKit 同步创建了重复/孤立的关系:
// 1. 备份当前状态
let backup = user.following.map { $0.id }
// 2. 清除关系
user.following.removeAll()
user.followers.removeAll()
try modelContext.save()
// 3. 从事实来源(例如 API)重建
for followingId in backup {
if let followingUser = fetchUser(id: followingId) {
user.following.append(followingUser)
}
}
try modelContext.save()
// 4. 强制 CloudKit 重新同步(在 ModelConfiguration 中)
// 重新创建 ModelContainer 以在损坏恢复后强制完全同步
.cascade - 删除相关对象.nullify - 将关系设置为 nil.deny - 如果关系存在则阻止删除.noAction - 保持关系不变(小心!)SwiftData 支持分层模型的类继承。当你有明确的 IS-A 关系(例如,BusinessTrip IS-A Trip)并且需要广泛的查询(所有行程)和特定类型的查询时使用。
对基类和子类都应用 @Model。在基类上省略 final。
@Model class Trip {
@Attribute(.preserveValueOnDeletion)
var name: String
var destination: String
var startDate: Date
var endDate: Date
@Relationship(deleteRule: .cascade, inverse: \Accommodation.trip)
var accommodation: Accommodation?
init(name: String, destination: String, startDate: Date, endDate: Date) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
@Model class BusinessTrip: Trip {
var purpose: String
var expenseCode: String
@Relationship(deleteRule: .cascade, inverse: \BusinessMeal.trip)
var businessMeals: [BusinessMeal] = []
init(name: String, destination: String, startDate: Date, endDate: Date,
purpose: String, expenseCode: String) {
self.purpose = purpose
self.expenseCode = expenseCode
super.init(name: name, destination: destination, startDate: startDate, endDate: endDate)
}
}
查询所有基类实例(包括子类),或按类型筛选:
// 所有行程(包括 BusinessTrip、PersonalTrip 等)
@Query(sort: \Trip.startDate) var allTrips: [Trip]
// 仅商务行程 — 在 #Predicate 中使用 `is`
@Query(filter: #Predicate<Trip> { $0 is BusinessTrip }) var businessTrips: [Trip]
// 基于子类特定属性筛选 — 使用 `as?` 转换
let vacationPredicate = #Predicate<Trip> {
if let personal = $0 as? PersonalTrip {
return personal.reason == .vacation
}
return false
}
@Query(filter: vacationPredicate) var vacationTrips: [Trip]
类型化为基类的关系可以包含混合的子类实例:
@Model class TravelPlanner {
var name: String
@Relationship(deleteRule: .cascade)
var upcomingTrips: [Trip] = [] // 可以包含 BusinessTrip 和 PersonalTrip
init(name: String) { self.name = name }
}
转换以访问子类特定属性:
for trip in planner.upcomingTrips {
if let business = trip as? BusinessTrip {
print(business.expenseCode)
}
}
| 信号 | 使用继承 | 使用枚举/标志代替 |
|---|---|---|
| 子类共享许多基类属性 | 是 | — |
| 需要跨所有模型的基于类型的查询 | 是 | — |
| 子类有自己的关系 | 是 | — |
| 只有 1-2 个区分属性 | — | 是 |
| 仅在特定属性上查询 | — | 是 |
| 协议一致性足够 | — | 是 |
保持层次结构浅(1-2 层)。深层链会使模式迁移和查询复杂化。
import SwiftUI
import SwiftData
@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Track.self, Album.self])
}
}
let schema = Schema([Track.self, Album.self])
let config = ModelConfiguration(
schema: schema,
url: URL(fileURLWithPath: "/path/to/database.sqlite"),
cloudKitDatabase: .private("iCloud.com.example.app")
)
let container = try ModelContainer(
for: schema,
configurations: config
)
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: schema,
configurations: config
)
import SwiftUI
import SwiftData
struct TracksView: View {
@Query var tracks: [Track]
var body: some View {
List(tracks) { track in
Text(track.title)
}
}
}
自动更新 数据更改时视图刷新。
// 筛选
@Query(filter: #Predicate<Track> { $0.genre == "Rock" }) var rockTracks: [Track]
// 排序(单个)
@Query(sort: \.title, order: .forward) var tracks: [Track]
// 排序(多个描述符)
@Query(sort: [SortDescriptor(\.artist), SortDescriptor(\.title)]) var tracks: [Track]
// 组合筛选 + 排序
@Query(filter: #Predicate<Track> { $0.duration > 180 }, sort: \.title) var longTracks: [Track]
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// ...
}
// 插入
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// 获取
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.genre == "Rock" },
sortBy: [SortDescriptor(\.title)]
)
let rockTracks = try modelContext.fetch(descriptor)
// 更新 — 只需修改属性,SwiftData 跟踪更改
track.title = "Updated Title"
// 删除
modelContext.delete(track)
// 批量删除
try modelContext.delete(model: Track.self, where: #Predicate { $0.genre == "Classical" })
// 保存(可选 — 视图消失时自动保存)
try modelContext.save()
#Predicate<Track> { $0.duration > 180 }
#Predicate<Track> { $0.artist == "Artist Name" }
#Predicate<Track> { $0.genre != nil }
#Predicate<Track> { track in
track.genre == "Rock" && track.duration > 180
}
#Predicate<Track> { track in
track.artist == "Artist" || track.artist == "Other Artist"
}
// 包含
#Predicate<Track> { track in
track.title.contains("Love")
}
// 不区分大小写的包含
#Predicate<Track> { track in
track.title.localizedStandardContains("love")
}
// 以...开头
#Predicate<Track> { track in
track.artist.hasPrefix("The ")
}
#Predicate<Track> { track in
track.album?.title == "Album Name"
}
#Predicate<Album> { album in
album.tracks.count > 10
}
import SwiftData
@MainActor
@Model
final class Track {
var id: String
var title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
}
原因 SwiftData 模型不是 Sendable。使用 @MainActor 确保从 SwiftUI 安全访问。
import SwiftData
actor DataImporter {
let modelContainer: ModelContainer
init(container: ModelContainer) {
self.modelContainer = container
}
func importTracks(_ tracks: [TrackData]) async throws {
// 创建后台上下文
let context = ModelContext(modelContainer)
for track in tracks {
let model = Track(
id: track.id,
title: track.title,
artist: track.artist,
duration: track.duration
)
context.insert(model)
}
try context.save()
}
}
模式 使用 ModelContext(modelContainer) 进行后台操作,而不是 @Environment(\.modelContext),后者绑定到主执行器。
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
Button("Import") {
Task {
let importer = DataImporter(container: modelContext.container)
try await importer.importTracks(data)
}
}
}
}
let schema = Schema([Track.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(
for: schema,
configurations: config
)
iCloud.com.example.MusicApp注意 SwiftData CloudKit 同步是自动的 - 无需手动解决冲突。
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString // ✅ 有默认值
var title: String = "" // ✅ 有默认值
var duration: TimeInterval = 0 // ✅ 有默认值
var genre: String? = nil // ✅ 可选
// ❌ 这些不适用于 CloudKit:
// var requiredField: String // 无默认值,不可选
}
原因 CloudKit 仅同步到私有区域,网络延迟意味着新记录可能尚未填充所有字段。
关系约束 所有关系必须是可选的
@Model
final class Track {
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album? // ✅ 对于 CloudKit 必须是可选的
}
SwiftData CloudKit 同步默认使用最后写入获胜策略。有关同步状态监控、自定义冲突解决和离线感知 UI 模式,请参阅 axiom-cloud-sync。有关基于 CKShare 的记录共享,请参阅 axiom-cloudkit-ref。
问题 尝试使用 CloudKit 同步时遇到此错误:
Property 'title' must be optional or have a default value for CloudKit synchronization
// ❌ 错误 - 必需属性
@Model
final class Track {
var title: String
}
// ✅ 正确 - 有默认值
@Model
final class Track {
var title: String = ""
}
// ✅ 也正确 - 可选
@Model
final class Track {
var title: String?
}
let schema = Schema([Track.self])
// 测试配置(无 CloudKit 同步)
let testConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: testConfig)
@Model
final class Track {
@Relationship(
deleteRule: .cascade,
inverse: \Album.tracks,
minimum: 0,
maximum: 1 // Track 最多属于一个专辑
) var album: Album?
}
@Model
final class Track {
var id: String
var duration: TimeInterval
@Transient
var formattedDuration: String {
let minutes = Int(duration) / 60
let seconds = Int(duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
瞬态 计算属性,不持久化。
// 启用历史追踪
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.app"),
allowsSave: true,
isHistoryEnabled: true // iOS 26+
)
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
descriptor.fetchLimit = 100 // 分页结果
let tracks = try modelContext.fetch(descriptor)
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album] // 急切加载专辑
let tracks = try modelContext.fetch(descriptor)
// 无 N+1 查询 - 专辑已加载
关键 没有预取,在循环中访问 track.album.title 会为每个轨道触发单独的查询:
// ❌ 慢:N+1 查询(1 次获取轨道 + 100 次获取专辑)
let tracks = try modelContext.fetch(FetchDescriptor<Track>())
for track in tracks {
print(track.album?.title) // 100 次单独的查询!
}
// ✅ 快:总共 2 次查询(1 次获取轨道 + 1 次获取所有专辑)
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album]
let tracks = try modelContext.fetch(descriptor)
for track in tracks {
print(track.album?.title) // 已加载
}
SwiftData 默认使用故障(延迟加载):
let track = tracks.first
// Album 是一个故障 - 尚未加载
let albumTitle = track.album?.title
// 访问时加载专辑(单独的查询)
// ❌ 慢:1000 次单独的保存
for track in largeDataset {
track.genre = "Updated"
try modelContext.save() // 昂贵 - 1000 次
}
// ✅ 快:单次保存操作
for track in largeDataset {
track.genre = "Updated"
}
try modelContext.save() // 整个批次一次
在频繁查询的属性上创建索引:
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString
@Attribute(.indexed) // ✅ 添加索引
var genre: String = ""
@Attribute(.indexed)
var releaseDate: Date = Date()
var title: String = ""
var duration: TimeInterval = 0
}
// 现在这些查询更快:
@Query(filter: #Predicate { $0.genre == "Rock" }) var rockTracks: [Track]
@Query(filter: #Predicate { $0.releaseDate > Date() }) var upcomingTracks: [Track]
@Query 筛选器中频繁使用的属性对于非常大的数据集(10万条以上记录),分块获取:
actor DataImporter {
let modelContainer: ModelContainer
func importLargeDataset(_ items: [Item]) async throws {
let chunkSize = 1000
let context = ModelContext(modelContainer)
for chunk in items.chunked(into: chunkSize) {
for item in chunk {
let track = Track(
id: item.id,
title: item.title,
artist: item.artist,
duration: item.duration
)
context.insert(track)
}
try context.save() // 每个分块后保存
// 防止内存膨胀
context.delete(model: Track.self, where: #Predicate { _ in true })
}
}
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}
使用 CloudKit 时,避免在闭包中捕获 self:
// ❌ 与 CloudKit 同步的循环引用
actor TrackManager {
func startSync() {
Task {
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
self.refreshUI() // 潜在的循环引用
}
}
}
}
// ✅ 正确的弱捕获
actor TrackManager {
func startSync() {
Task { [weak self] in
guard let self else { return }
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
await self.refreshUI()
}
}
}
}
struct SearchableTracksView: View {
@Query var tracks: [Track]
@State private var searchText = ""
var filteredTracks: [Track] {
if searchText.isEmpty {
return tracks
}
return tracks.filter { track in
track.title.localizedStandardContains(searchText) ||
track.artist.localizedStandardContains(searchText)
}
}
var body: some View {
List(filteredTracks) { track in
Text(track.title)
}
.searchable(text: $searchText)
}
}
struct TracksView: View {
@Query var tracks: [Track]
@State private var sortOrder: SortOrder = .title
enum SortOrder {
case title, artist, duration
}
var sortedTracks: [Track] {
switch sortOrder {
case .title:
return tracks.sorted { $0.title < $1.title }
case .artist:
return tracks.sorted { $0.artist < $1.artist }
case .duration:
return tracks.sorted { $0.duration < $1.duration }
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.undoManager) private var undoManager
func deleteTrack(_ track: Track) {
modelContext.delete(track)
// 撤销是自动的,使用 modelContext
// 使用 Cmd+Z 撤销
}
}
| 概念 | Realm | Core Data | SwiftData |
|---|---|---|---|
| 模型定义 | Object 子类 + @Persisted | NSManagedObject + @NSManaged | final class + @Model |
| 主键 | @Persisted(primaryKey:) | 实体检查器 | @Attribute(.unique) |
| 线程处理 | 手动每个线程的 Realm 实例 | context.perform {} 块 | 执行器隔离 + ModelContext(container) |
| 关系 | RealmSwiftCollection<T> | 实体编辑器 + @NSManaged | @Relationship 带自动反向关系 |
| 后台工作 | DispatchQueue + 线程本地 Realm | newBackgroundContext() | actor + ModelContext(modelContainer) |
| 批量删除 | 循环 + realm.delete() | NSBatchDeleteRequest | context.delete(model:where:) |
| CloudKit 同步 | Realm Sync(2025年9月已弃用) | NSPersistentCloudKitContainer | ModelConfiguration(cloudKitDatabase:) |
realm-to-swiftdata-migration — 完整的 Realm 迁移:模式等效、线程安全转换、关系迁移、CloudKit 同步过渡、时间线规划axiom-swiftdata-migration — SwiftData 模式演进:VersionedSchema、SchemaMigrationPlan、轻量级与自定义迁移axiom-database-migration — 适用于任何持久化框架的安全增量迁移模式import XCTest
import SwiftData
@testable import MusicApp
final class TrackTests: XCTestCase {
var modelContext: ModelContext!
override func setUp() async throws {
let schema = Schema([Track.self])
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: config)
modelContext = ModelContext(container)
}
func testInsertTrack() throws {
let track = Track(id: "1", title: "Test", artist: "Artist", duration: 240)
modelContext.insert(track)
let descriptor = FetchDescriptor<Track>()
let tracks = try modelContext.fetch(descriptor)
XCTAssertEqual(tracks.count, 1)
XCTAssertEqual(tracks.first?.title, "Test")
}
}
| 功能 | SwiftData | SQLiteData |
|---|---|---|
| 类型 | 引用(类) | 值(结构体) |
| 宏 | @Model | @Table |
| 查询 | SwiftUI 中的 @Query | @FetchAll / @FetchOne |
| 关系 | @Relationship 宏 | 显式外键 |
| CloudKit | 自动同步 | 手动 SyncEngine + 共享 |
| 后端 | Core Data | GRDB + SQLite |
| 学习曲线 | 简单(原生) | 中等 |
| 性能 | 良好 | 优秀(原始 SQL) |
tvOS 上的 SwiftData 没有持久化的本地存储。 tvOS 没有文档目录,应用程序支持映射到缓存 — 系统在存储压力下会删除文件。仅本地的 SwiftData 存储将丢失所有数据。
你必须使用 CloudKit 同步(cloudKitDatabase: .private(...))用于 tvOS SwiftData 应用。没有 iCloud,用户数据在应用启动之间无法保留。有关完整的 tvOS 存储约束,请参阅 axiom-tvos。
文档 : /swiftdata, /swiftdata/adopting-inheritance-in-swiftdata
技能 : axiom-swiftdata-migration, axiom-swiftdata-migration-diag, axiom-database-migration, axiom-sqlitedata, axiom-grdb, axiom-swift-concurrency
@Model
final class Track {
var id: String
var title: String
// 没有 init - 无法编译
}
修复 始终为 @Model 类提供 init
@Model
struct Track { } // 无效 - 必须是类
修复 使用 final class 而不是 struct
@Environment(\.modelContext) var context // 仅主执行器
Task {
// ❌ 崩溃 - 跨越执行器边界
context.insert(track)
}
修复 使用 ModelContext(modelContainer) 进行后台工作
modelContext.insert(track)
// 可能不会立即持久化
修复 调用 try modelContext.save() 以立即持久化
创建于 2025-11-28 目标 iOS 17+(侧重于 iOS 26+ 功能) 框架 SwiftData(Apple) Swift 5.9+(Swift 6 并发模式)
每周安装数
241
仓库
GitHub 星标数
674
首次出现
Jan 21, 2026
安全审计
安装于
opencode217
codex212
gemini-cli207
github-copilot199
claude-code198
cursor185
Apple's native persistence framework using @Model classes and declarative queries. Built on Core Data, designed for SwiftUI.
Core principle Reference types (class) + @Model macro + declarative @Query for reactive SwiftUI integration.
Requires iOS 17+, Swift 5.9+ Target iOS 26+ (this skill focuses on latest features) License Proprietary (Apple)
@QueryFor migrations See the axiom-swiftdata-migration skill for custom schema migrations with VersionedSchema and SchemaMigrationPlan. For migration debugging, see axiom-swiftdata-migration-diag.
These are real questions developers ask that this skill is designed to answer:
→ The skill shows how to use @Query with predicates, sorting, and automatic view updates
→ The skill explains @Relationship with deleteRule: .cascade and inverse relationships
→ The skill shows cascading deletes, inverse relationships, and safe deletion patterns
→ The skill covers CloudKit integration, conflict resolution strategies (last-write-wins, custom resolution), and sync patterns
→ The skill explains CloudKit constraints: all properties must be optional or have defaults, explains why (network timing), and shows fixes
→ The skill shows monitoring sync status with notifications, detecting network connectivity, and offline-aware UI patterns
→ The skill covers CloudKit record sharing patterns (iOS 26+) with owner/permission tracking and sharing metadata
→ The skill covers performance patterns, batch fetching, limiting queries, and preventing memory bloat with chunked imports
→ The skill shows how to identify N+1 problems without prefetching, provides prefetching pattern, and shows 100x performance improvement
→ The skill shows chunk-based importing with periodic saves, memory cleanup patterns, and batch operation optimization
→ The skill explains index optimization patterns: when to index (frequently filtered/sorted properties), when to avoid (rarely used, frequently changing), maintenance costs
→ See the comparison table in Migration section below, then follow realm-to-swiftdata-migration or axiom-swiftdata-migration for detailed guides
import SwiftData
@Model
final class Track {
@Attribute(.unique) var id: String
var title: String
var artist: String
var duration: TimeInterval
var genre: String?
init(id: String, title: String, artist: String, duration: TimeInterval, genre: String? = nil) {
self.id = id
self.title = title
self.artist = artist
self.duration = duration
self.genre = genre
}
}
final class, not struct (omit final if you need subclasses — see Class Inheritance below)@Attribute(.unique) for primary key-like behaviorinit (SwiftData doesn't synthesize)String?) are nullable@Attribute(.preserveValueOnDeletion) on properties whose values should survive even after the object is deleted (useful for analytics, audit trails)@Model
final class Track {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album?
init(id: String, title: String, album: Album? = nil) {
self.id = id
self.title = title
self.album = album
}
}
@Model
final class Album {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade)
var tracks: [Track] = []
init(id: String, title: String) {
self.id = id
self.title = title
}
}
@MainActor // Required for Swift 6 strict concurrency
@Model
final class User {
@Attribute(.unique) var id: String
var name: String
// Users following this user (inverse relationship)
@Relationship(deleteRule: .nullify, inverse: \User.following)
var followers: [User] = []
// Users this user is following
@Relationship(deleteRule: .nullify)
var following: [User] = []
init(id: String, name: String) {
self.id = id
self.name = name
}
}
✅ Correct — Only modify ONE side
// user1 follows user2 (modifying ONE side)
user1.following.append(user2)
try modelContext.save()
// SwiftData AUTOMATICALLY updates user2.followers
// Don't manually append to both sides - causes duplicates!
❌ Wrong — Don't manually update both sides
user1.following.append(user2)
user2.followers.append(user1) // Redundant! Creates duplicates in CloudKit sync
user1.following.removeAll { $0.id == user2.id }
try modelContext.save()
// user2.followers automatically updated
// Check if relationship is truly bidirectional
let user1FollowsUser2 = user1.following.contains { $0.id == user2.id }
let user2FollowedByUser1 = user2.followers.contains { $0.id == user1.id }
// These MUST always match after save()
assert(user1FollowsUser2 == user2FollowedByUser1, "Relationship corrupted!")
// If CloudKit sync creates duplicate/orphaned relationships:
// 1. Backup current state
let backup = user.following.map { $0.id }
// 2. Clear relationships
user.following.removeAll()
user.followers.removeAll()
try modelContext.save()
// 3. Rebuild from source of truth (e.g., API)
for followingId in backup {
if let followingUser = fetchUser(id: followingId) {
user.following.append(followingUser)
}
}
try modelContext.save()
// 4. Force CloudKit resync (in ModelConfiguration)
// Re-create ModelContainer to force full sync after corruption recovery
.cascade - Delete related objects.nullify - Set relationship to nil.deny - Prevent deletion if relationship exists.noAction - Leave relationship as-is (careful!)SwiftData supports class inheritance for hierarchical models. Use when you have a clear IS-A relationship (e.g., BusinessTrip IS-A Trip) and need both broad queries (all trips) and type-specific queries.
Apply @Model to both base class and subclasses. Omit final on the base class.
@Model class Trip {
@Attribute(.preserveValueOnDeletion)
var name: String
var destination: String
var startDate: Date
var endDate: Date
@Relationship(deleteRule: .cascade, inverse: \Accommodation.trip)
var accommodation: Accommodation?
init(name: String, destination: String, startDate: Date, endDate: Date) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
@Model class BusinessTrip: Trip {
var purpose: String
var expenseCode: String
@Relationship(deleteRule: .cascade, inverse: \BusinessMeal.trip)
var businessMeals: [BusinessMeal] = []
init(name: String, destination: String, startDate: Date, endDate: Date,
purpose: String, expenseCode: String) {
self.purpose = purpose
self.expenseCode = expenseCode
super.init(name: name, destination: destination, startDate: startDate, endDate: endDate)
}
}
Query all base class instances (includes subclasses), or filter by type:
// All trips (includes BusinessTrip, PersonalTrip, etc.)
@Query(sort: \Trip.startDate) var allTrips: [Trip]
// Only business trips — use `is` in #Predicate
@Query(filter: #Predicate<Trip> { $0 is BusinessTrip }) var businessTrips: [Trip]
// Filter on subclass-specific properties — use `as?` cast
let vacationPredicate = #Predicate<Trip> {
if let personal = $0 as? PersonalTrip {
return personal.reason == .vacation
}
return false
}
@Query(filter: vacationPredicate) var vacationTrips: [Trip]
Relationships typed to the base class can hold mixed subclass instances:
@Model class TravelPlanner {
var name: String
@Relationship(deleteRule: .cascade)
var upcomingTrips: [Trip] = [] // Can contain BusinessTrip and PersonalTrip
init(name: String) { self.name = name }
}
Cast to access subclass-specific properties:
for trip in planner.upcomingTrips {
if let business = trip as? BusinessTrip {
print(business.expenseCode)
}
}
| Signal | Use Inheritance | Use Enum/Flag Instead |
|---|---|---|
| Subclasses share many base properties | Yes | — |
| Need type-based queries across all models | Yes | — |
| Subclasses have their own relationships | Yes | — |
| Only 1-2 distinguishing properties | — | Yes |
| Query only on specialized properties | — | Yes |
| Protocol conformance suffices | — | Yes |
Keep hierarchies shallow (1-2 levels). Deep chains complicate schema migrations and queries.
import SwiftUI
import SwiftData
@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Track.self, Album.self])
}
}
let schema = Schema([Track.self, Album.self])
let config = ModelConfiguration(
schema: schema,
url: URL(fileURLWithPath: "/path/to/database.sqlite"),
cloudKitDatabase: .private("iCloud.com.example.app")
)
let container = try ModelContainer(
for: schema,
configurations: config
)
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: schema,
configurations: config
)
import SwiftUI
import SwiftData
struct TracksView: View {
@Query var tracks: [Track]
var body: some View {
List(tracks) { track in
Text(track.title)
}
}
}
Automatic updates View refreshes when data changes.
// Filtered
@Query(filter: #Predicate<Track> { $0.genre == "Rock" }) var rockTracks: [Track]
// Sorted (single)
@Query(sort: \.title, order: .forward) var tracks: [Track]
// Sorted (multiple descriptors)
@Query(sort: [SortDescriptor(\.artist), SortDescriptor(\.title)]) var tracks: [Track]
// Combined filter + sort
@Query(filter: #Predicate<Track> { $0.duration > 180 }, sort: \.title) var longTracks: [Track]
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// ...
}
// Insert
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// Fetch
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.genre == "Rock" },
sortBy: [SortDescriptor(\.title)]
)
let rockTracks = try modelContext.fetch(descriptor)
// Update — just modify properties, SwiftData tracks changes
track.title = "Updated Title"
// Delete
modelContext.delete(track)
// Batch delete
try modelContext.delete(model: Track.self, where: #Predicate { $0.genre == "Classical" })
// Save (optional — auto-saves on view disappear)
try modelContext.save()
#Predicate<Track> { $0.duration > 180 }
#Predicate<Track> { $0.artist == "Artist Name" }
#Predicate<Track> { $0.genre != nil }
#Predicate<Track> { track in
track.genre == "Rock" && track.duration > 180
}
#Predicate<Track> { track in
track.artist == "Artist" || track.artist == "Other Artist"
}
// Contains
#Predicate<Track> { track in
track.title.contains("Love")
}
// Case-insensitive contains
#Predicate<Track> { track in
track.title.localizedStandardContains("love")
}
// Starts with
#Predicate<Track> { track in
track.artist.hasPrefix("The ")
}
#Predicate<Track> { track in
track.album?.title == "Album Name"
}
#Predicate<Album> { album in
album.tracks.count > 10
}
import SwiftData
@MainActor
@Model
final class Track {
var id: String
var title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
}
Why SwiftData models are not Sendable. Use @MainActor to ensure safe access from SwiftUI.
import SwiftData
actor DataImporter {
let modelContainer: ModelContainer
init(container: ModelContainer) {
self.modelContainer = container
}
func importTracks(_ tracks: [TrackData]) async throws {
// Create background context
let context = ModelContext(modelContainer)
for track in tracks {
let model = Track(
id: track.id,
title: track.title,
artist: track.artist,
duration: track.duration
)
context.insert(model)
}
try context.save()
}
}
Pattern Use ModelContext(modelContainer) for background operations, not @Environment(\.modelContext) which is main-actor bound.
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
Button("Import") {
Task {
let importer = DataImporter(container: modelContext.container)
try await importer.importTracks(data)
}
}
}
}
let schema = Schema([Track.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(
for: schema,
configurations: config
)
iCloud.com.example.MusicAppNote SwiftData CloudKit sync is automatic - no manual conflict resolution needed.
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString // ✅ Has default
var title: String = "" // ✅ Has default
var duration: TimeInterval = 0 // ✅ Has default
var genre: String? = nil // ✅ Optional
// ❌ These don't work with CloudKit:
// var requiredField: String // No default, not optional
}
Why CloudKit only syncs to private zones, and network delays mean new records may not have all fields populated yet.
Relationship Constraint All relationships must be optional
@Model
final class Track {
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album? // ✅ Must be optional for CloudKit
}
SwiftData CloudKit sync uses last-write-wins by default. For sync status monitoring, custom conflict resolution, and offline-aware UI patterns, see axiom-cloud-sync. For CKShare-based record sharing, see axiom-cloudkit-ref.
Problem You get this error when trying to use CloudKit sync:
Property 'title' must be optional or have a default value for CloudKit synchronization
// ❌ Wrong - required property
@Model
final class Track {
var title: String
}
// ✅ Correct - has default
@Model
final class Track {
var title: String = ""
}
// ✅ Also correct - optional
@Model
final class Track {
var title: String?
}
let schema = Schema([Track.self])
// Test configuration (no CloudKit sync)
let testConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: testConfig)
@Model
final class Track {
@Relationship(
deleteRule: .cascade,
inverse: \Album.tracks,
minimum: 0,
maximum: 1 // Track belongs to at most one album
) var album: Album?
}
@Model
final class Track {
var id: String
var duration: TimeInterval
@Transient
var formattedDuration: String {
let minutes = Int(duration) / 60
let seconds = Int(duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
Transient Computed property, not persisted.
// Enable history tracking
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.app"),
allowsSave: true,
isHistoryEnabled: true // iOS 26+
)
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
descriptor.fetchLimit = 100 // Paginate results
let tracks = try modelContext.fetch(descriptor)
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album] // Eager load album
let tracks = try modelContext.fetch(descriptor)
// No N+1 queries - albums already loaded
CRITICAL Without prefetching, accessing track.album.title in a loop triggers individual queries for EACH track:
// ❌ SLOW: N+1 queries (1 fetch tracks + 100 fetch albums)
let tracks = try modelContext.fetch(FetchDescriptor<Track>())
for track in tracks {
print(track.album?.title) // 100 separate queries!
}
// ✅ FAST: 2 queries total (1 fetch tracks + 1 fetch all albums)
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album]
let tracks = try modelContext.fetch(descriptor)
for track in tracks {
print(track.album?.title) // Already loaded
}
SwiftData uses faulting (lazy loading) by default:
let track = tracks.first
// Album is a fault - not loaded yet
let albumTitle = track.album?.title
// Album loaded on access (separate query)
// ❌ SLOW: 1000 individual saves
for track in largeDataset {
track.genre = "Updated"
try modelContext.save() // Expensive - 1000 times
}
// ✅ FAST: Single save operation
for track in largeDataset {
track.genre = "Updated"
}
try modelContext.save() // Once for entire batch
Create indexes on frequently queried properties:
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString
@Attribute(.indexed) // ✅ Add index
var genre: String = ""
@Attribute(.indexed)
var releaseDate: Date = Date()
var title: String = ""
var duration: TimeInterval = 0
}
// Now these queries are faster:
@Query(filter: #Predicate { $0.genre == "Rock" }) var rockTracks: [Track]
@Query(filter: #Predicate { $0.releaseDate > Date() }) var upcomingTracks: [Track]
@Query filters frequentlyFor very large datasets (100k+ records), fetch in chunks:
actor DataImporter {
let modelContainer: ModelContainer
func importLargeDataset(_ items: [Item]) async throws {
let chunkSize = 1000
let context = ModelContext(modelContainer)
for chunk in items.chunked(into: chunkSize) {
for item in chunk {
let track = Track(
id: item.id,
title: item.title,
artist: item.artist,
duration: item.duration
)
context.insert(track)
}
try context.save() // Save after each chunk
// Prevent memory bloat
context.delete(model: Track.self, where: #Predicate { _ in true })
}
}
}
extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}
When using CloudKit, avoid capturing self in closures:
// ❌ Retain cycle with CloudKit sync
actor TrackManager {
func startSync() {
Task {
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
self.refreshUI() // Potential retain cycle
}
}
}
}
// ✅ Proper weak capture
actor TrackManager {
func startSync() {
Task { [weak self] in
guard let self else { return }
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
await self.refreshUI()
}
}
}
}
struct SearchableTracksView: View {
@Query var tracks: [Track]
@State private var searchText = ""
var filteredTracks: [Track] {
if searchText.isEmpty {
return tracks
}
return tracks.filter { track in
track.title.localizedStandardContains(searchText) ||
track.artist.localizedStandardContains(searchText)
}
}
var body: some View {
List(filteredTracks) { track in
Text(track.title)
}
.searchable(text: $searchText)
}
}
struct TracksView: View {
@Query var tracks: [Track]
@State private var sortOrder: SortOrder = .title
enum SortOrder {
case title, artist, duration
}
var sortedTracks: [Track] {
switch sortOrder {
case .title:
return tracks.sorted { $0.title < $1.title }
case .artist:
return tracks.sorted { $0.artist < $1.artist }
case .duration:
return tracks.sorted { $0.duration < $1.duration }
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.undoManager) private var undoManager
func deleteTrack(_ track: Track) {
modelContext.delete(track)
// Undo is automatic with modelContext
// Use Cmd+Z to undo
}
}
| Concept | Realm | Core Data | SwiftData |
|---|---|---|---|
| Model definition | Object subclass + @Persisted | NSManagedObject + @NSManaged | final class + @Model |
| Primary key | @Persisted(primaryKey:) | Entity inspector |
realm-to-swiftdata-migration — Complete Realm migration: pattern equivalents, thread safety conversion, relationship migration, CloudKit sync transition, timeline planningaxiom-swiftdata-migration — SwiftData schema evolution: VersionedSchema, SchemaMigrationPlan, lightweight vs custom migrationsaxiom-database-migration — Safe additive migration patterns applicable to any persistence frameworkimport XCTest
import SwiftData
@testable import MusicApp
final class TrackTests: XCTestCase {
var modelContext: ModelContext!
override func setUp() async throws {
let schema = Schema([Track.self])
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: config)
modelContext = ModelContext(container)
}
func testInsertTrack() throws {
let track = Track(id: "1", title: "Test", artist: "Artist", duration: 240)
modelContext.insert(track)
let descriptor = FetchDescriptor<Track>()
let tracks = try modelContext.fetch(descriptor)
XCTAssertEqual(tracks.count, 1)
XCTAssertEqual(tracks.first?.title, "Test")
}
}
| Feature | SwiftData | SQLiteData |
|---|---|---|
| Type | Reference (class) | Value (struct) |
| Macro | @Model | @Table |
| Queries | @Query in SwiftUI | @FetchAll / @FetchOne |
| Relationships | @Relationship macro |
SwiftData on tvOS has no persistent local storage. tvOS has no Document directory, and Application Support maps to Caches — the system deletes files under storage pressure. A local-only SwiftData store will lose all data.
You must use CloudKit sync (cloudKitDatabase: .private(...)) for tvOS SwiftData apps. Without iCloud, user data does not survive between app launches. See axiom-tvos for full tvOS storage constraints.
Docs : /swiftdata, /swiftdata/adopting-inheritance-in-swiftdata
Skills : axiom-swiftdata-migration, axiom-swiftdata-migration-diag, axiom-database-migration, axiom-sqlitedata, axiom-grdb, axiom-swift-concurrency
@Model
final class Track {
var id: String
var title: String
// No init - won't compile
}
Fix Always provide init for @Model classes
@Model
struct Track { } // Won't work - must be class
Fix Use final class not struct
@Environment(\.modelContext) var context // Main actor only
Task {
// ❌ Crash - crossing actor boundaries
context.insert(track)
}
Fix Use ModelContext(modelContainer) for background work
modelContext.insert(track)
// Might not persist immediately
Fix Call try modelContext.save() for immediate persistence
Created 2025-11-28 Targets iOS 17+ (focus on iOS 26+ features) Framework SwiftData (Apple) Swift 5.9+ (Swift 6 concurrency patterns)
Weekly Installs
241
Repository
GitHub Stars
674
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode217
codex212
gemini-cli207
github-copilot199
claude-code198
cursor185
SQL查询优化指南:PostgreSQL、Snowflake、BigQuery高性能SQL编写技巧与方言参考
811 周安装
Vitest 3.x 测试框架:Vite 驱动的下一代 JavaScript/TypeScript 测试工具
236 周安装
推荐与联盟计划设计优化指南:病毒式增长、客户获取成本降低策略
236 周安装
使用 shadcn/ui 和 Radix Primitives 构建无障碍 UI 组件库 - CVA 变体与 OKLCH 主题指南
236 周安装
use-agently CLI:去中心化AI智能体市场命令行工具,支持A2A/MCP协议与链上支付
237 周安装
Docker容器化最佳实践指南:生产就绪容器构建、安全优化与CI/CD部署
237 周安装
Excel/XLSX文件编程操作指南:Python openpyxl/pandas与JavaScript xlsx库教程
237 周安装
@Attribute(.unique)| Threading | Manual per-thread Realm instances | context.perform {} blocks | Actor isolation + ModelContext(container) |
| Relationships | RealmSwiftCollection<T> | Entity editor + @NSManaged | @Relationship with automatic inverses |
| Background work | DispatchQueue + thread-local Realm | newBackgroundContext() | actor + ModelContext(modelContainer) |
| Batch delete | Loop + realm.delete() | NSBatchDeleteRequest | context.delete(model:where:) |
| CloudKit sync | Realm Sync (deprecated Sept 2025) | NSPersistentCloudKitContainer | ModelConfiguration(cloudKitDatabase:) |
| Explicit foreign keys |
| CloudKit | Automatic sync | Manual SyncEngine + sharing |
| Backend | Core Data | GRDB + SQLite |
| Learning Curve | Easy (native) | Moderate |
| Performance | Good | Excellent (raw SQL) |