axiom-swiftdata
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftData
SwiftData
Overview
概述
Apple's native persistence framework using classes and declarative queries. Built on Core Data, designed for SwiftUI.
@ModelCore principle Reference types () + macro + declarative for reactive SwiftUI integration.
class@Model@QueryRequires iOS 17+, Swift 5.9+
Target iOS 26+ (this skill focuses on latest features)
License Proprietary (Apple)
这是苹果的原生持久化框架,使用类和声明式查询。基于Core Data构建,专为SwiftUI设计。
@Model核心原则 引用类型() + 宏 + 声明式,实现与SwiftUI的响应式集成。
class@Model@Query要求 iOS 17+、Swift 5.9+
目标版本 iOS 26+(本技能聚焦最新特性)
许可协议 专有协议(苹果)
When to Use SwiftData
何时使用SwiftData
Choose SwiftData when you need
以下场景选择SwiftData
- ✅ Native Apple integration with SwiftUI
- ✅ Simple CRUD operations
- ✅ Automatic UI updates with
@Query - ✅ CloudKit sync (iOS 17+)
- ✅ Reference types (classes) with relationships
- ✅ 与SwiftUI的原生苹果集成
- ✅ 简单的CRUD操作
- ✅ 借助实现UI自动更新
@Query - ✅ CloudKit同步(iOS 17+)
- ✅ 带关系的引用类型(类)
Use SQLiteData instead when
以下场景改用SQLiteData
- Need value types (structs)
- CloudKit record sharing (not just sync)
- Large datasets (50k+ records) with specific performance needs
- 需要值类型(struct)
- CloudKit记录共享(而非仅同步)
- 大型数据集(5万+条记录)且有特定性能需求
Use GRDB when
以下场景改用GRDB
- Complex raw SQL required
- Fine-grained migration control needed
For migrations See the skill for custom schema migrations with VersionedSchema and SchemaMigrationPlan. For migration debugging, see .
axiom-swiftdata-migrationaxiom-swiftdata-migration-diag- 需要复杂的原生SQL
- 需要细粒度的迁移控制
迁移相关 如需自定义模式迁移(含VersionedSchema和SchemaMigrationPlan),请参考技能。如需迁移调试,请参考。
axiom-swiftdata-migrationaxiom-swiftdata-migration-diagExample Prompts
示例提问
These are real questions developers ask that this skill is designed to answer:
以下是开发者常问的、本技能可解答的问题:
Basic Operations
基础操作
1. "I have a notes app with folders. I need to filter notes by folder and sort by last modified. How do I set up the @Query?"
1. "我开发了一个带文件夹的笔记应用,需要按文件夹筛选笔记并按最后修改时间排序,该如何设置@Query?"
→ The skill shows how to use with predicates, sorting, and automatic view updates
@Query→ 本技能会展示如何结合谓词、排序和自动视图更新使用
@Query2. "When a user deletes a task list, all tasks should auto-delete too. How do I set up the relationship?"
2. "当用户删除任务列表时,所有关联任务也应自动删除,该如何设置关系?"
→ The skill explains with and inverse relationships
@RelationshipdeleteRule: .cascade→ 本技能会讲解带的以及反向关系
deleteRule: .cascade@Relationship3. "I have a relationship between User → Messages → Attachments. How do I prevent orphaned data when deleting?"
3. "我设置了User → Messages → Attachments的关系,删除时如何避免孤立数据?"
→ The skill shows cascading deletes, inverse relationships, and safe deletion patterns
→ 本技能会展示级联删除、反向关系和安全删除模式
CloudKit & Sync
CloudKit与同步
4. "My chat app syncs messages to other devices via CloudKit. Sometimes messages conflict. How do I handle sync conflicts?"
4. "我的聊天应用通过CloudKit在多设备间同步消息,有时会出现冲突,该如何处理同步冲突?"
→ The skill covers CloudKit integration, conflict resolution strategies (last-write-wins, custom resolution), and sync patterns
→ 本技能涵盖CloudKit集成、冲突解决策略(最后写入获胜、自定义解决)和同步模式
5. "I'm adding CloudKit sync to my app, but I get 'Property must have a default value' error. What's wrong?"
5. "我在应用中添加CloudKit同步时,遇到了'Property must have a default value'错误,问题出在哪?"
→ The skill explains CloudKit constraints: all properties must be optional or have defaults, explains why (network timing), and shows fixes
→ 本技能会解释CloudKit的约束:所有属性必须是可选类型或有默认值,说明原因(网络时序问题)并提供修复方案
6. "I want to show users when their data is syncing to iCloud and what happens when they're offline."
6. "我想向用户展示数据正在同步到iCloud的状态,以及离线时的处理逻辑。"
→ The skill shows monitoring sync status with notifications, detecting network connectivity, and offline-aware UI patterns
→ 本技能会展示如何通过通知监控同步状态、检测网络连接,以及实现离线感知的UI模式
7. "I need to share a playlist with other users. How do I implement CloudKit record sharing?"
7. "我需要实现播放列表的用户共享功能,该如何实现CloudKit记录共享?"
→ The skill covers CloudKit record sharing patterns (iOS 26+) with owner/permission tracking and sharing metadata
→ 本技能涵盖iOS 26+的CloudKit记录共享模式,包括所有者/权限跟踪和共享元数据
Performance & Optimization
性能与优化
8. "I need to query 50,000 messages but only display 20 at a time. How do I paginate efficiently?"
8. "我需要查询5万条消息,但每次只显示20条,如何高效实现分页?"
→ The skill covers performance patterns, batch fetching, limiting queries, and preventing memory bloat with chunked imports
→ 本技能涵盖性能模式、批量获取、查询限制,以及通过分段导入避免内存膨胀
9. "My app loads 100 tasks with relationships, and displaying them is slow. I think it's N+1 queries."
9. "我的应用加载100个带关系的任务,显示时速度很慢,我认为是N+1查询问题。"
→ The skill shows how to identify N+1 problems without prefetching, provides prefetching pattern, and shows 100x performance improvement
→ 本技能会展示如何在不使用预取的情况下识别N+1问题,提供预取模式,并展示100倍的性能提升效果
10. "I'm importing 1 million records from an API. What's the best way to batch them without running out of memory?"
10. "我要从API导入100万条记录,如何分批处理而不耗尽内存?"
→ The skill shows chunk-based importing with periodic saves, memory cleanup patterns, and batch operation optimization
→ 本技能会展示基于分段的导入方式,结合定期保存、内存清理模式和批量操作优化
11. "Which properties should I add indexes to? I'm worried about over-indexing slowing down writes."
11. "我应该为哪些属性添加索引?我担心过度索引会降低写入速度。"
→ The skill explains index optimization patterns: when to index (frequently filtered/sorted properties), when to avoid (rarely used, frequently changing), maintenance costs
→ 本技能会解释索引优化模式:何时添加索引(频繁筛选/排序的属性)、何时避免(很少使用、频繁变更的属性),以及索引维护成本
Migration from Legacy Frameworks
从旧框架迁移
12. "We're migrating from Realm to SwiftData. What are the biggest differences in how we write code?"
12. "我们正从Realm迁移到SwiftData,代码编写方式有哪些主要差异?"
→ The skill shows Realm → SwiftData pattern equivalents: @Persisted → @Attribute, threading model differences, relationship handling
→ 本技能会展示Realm与SwiftData的模式对应关系:@Persisted → @Attribute、线程模型差异、关系处理方式
13. "We have Core Data in production. What's the safest way to migrate to SwiftData while keeping both running?"
13. "我们的生产环境使用Core Data,在保留两者同时运行的前提下,迁移到SwiftData的最安全方式是什么?"
→ The skill covers dual-stack migration: reading Core Data, writing to SwiftData, marking migrated records, gradual cutover, validation
→ 本技能涵盖双栈迁移:读取Core Data数据、写入SwiftData、标记已迁移记录、逐步切换、验证
14. "Our Realm app uses background threads for all database operations. How do I convert to SwiftData's async/await model?"
14. "我们的Realm应用在后台线程处理所有数据库操作,如何转换为SwiftData的async/await模型?"
→ The skill explains thread-confinement migration: actor-based safety, removing manual DispatchQueue, proper async context patterns, Swift 6 concurrency
→ 本技能会解释线程约束迁移:基于Actor的安全机制、移除手动DispatchQueue、正确的异步上下文模式、Swift 6并发
15. "I need to migrate our CloudKit sync from Realm Sync (deprecated) to SwiftData CloudKit integration."
15. "我需要将Realm Sync(已废弃)的CloudKit同步迁移到SwiftData的CloudKit集成。"
→ The skill shows Realm Sync → SwiftData CloudKit migration, addressing sync feature gaps, testing new sync implementation
→ 本技能会展示Realm Sync到SwiftData CloudKit的迁移方法,解决同步功能差距,测试新的同步实现
@Model Definitions
@Model 定义
Basic Model
基础模型
swift
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
}
}swift
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
}
}Key patterns
关键模式
- Use , not
final classstruct - Use for primary key-like behavior
@Attribute(.unique) - Provide explicit (SwiftData doesn't synthesize)
init - Optional properties () are nullable
String?
- 使用,而非
final classstruct - 使用实现类似主键的行为
@Attribute(.unique) - 提供显式的(SwiftData不会自动生成)
init - 可选属性()支持为空
String?
Relationships
关系
swift
@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
}
}swift
@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
}
}Many-to-Many Self-Referential Relationships
多对多自引用关系
swift
@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
}
}swift
@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
}
}CRITICAL: SwiftData automatically manages BOTH sides when you modify ONE side.
重要提示:修改关系的一侧时,SwiftData会自动管理两侧。
✅ Correct — Only modify ONE side
swift
// 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
swift
user1.following.append(user2)
user2.followers.append(user1) // Redundant! Creates duplicates in CloudKit sync✅ 正确做法 — 仅修改一侧
swift
// user1 关注 user2(仅修改一侧)
user1.following.append(user2)
try modelContext.save()
// SwiftData会自动更新user2.followers
// 不要手动同时修改两侧,会导致重复!❌ 错误做法 — 不要手动更新两侧
swift
user1.following.append(user2)
user2.followers.append(user1) // 冗余!会在CloudKit同步中创建重复数据Unfollowing (remove from ONE side only)
取消关注(仅从一侧移除)
swift
user1.following.removeAll { $0.id == user2.id }
try modelContext.save()
// user2.followers automatically updatedswift
user1.following.removeAll { $0.id == user2.id }
try modelContext.save()
// user2.followers会自动更新Verifying relationship integrity (for debugging)
验证关系完整性(调试用)
swift
// 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!")swift
// 检查关系是否真正双向
let user1FollowsUser2 = user1.following.contains { $0.id == user2.id }
let user2FollowedByUser1 = user2.followers.contains { $0.id == user1.id }
// 保存后这两个值必须始终相等
assert(user1FollowsUser2 == user2FollowedByUser1, "Relationship corrupted!")CloudKit Sync Recovery (if relationships become corrupted)
CloudKit同步恢复(若关系损坏)
swift
// 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 recoveryswift
// 若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以强制全量同步Delete rules
删除规则
- - Delete related objects
.cascade - - Set relationship to nil
.nullify - - Prevent deletion if relationship exists
.deny - - Leave relationship as-is (careful!)
.noAction
- - 删除关联对象
.cascade - - 将关系设为nil
.nullify - - 若存在关联则阻止删除
.deny - - 保持关系不变(需谨慎!)
.noAction
ModelContainer Setup
ModelContainer 设置
SwiftUI App
SwiftUI应用
swift
import SwiftUI
import SwiftData
@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Track.self, Album.self])
}
}swift
import SwiftUI
import SwiftData
@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Track.self, Album.self])
}
}Custom Configuration
自定义配置
swift
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
)swift
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
)In-Memory (Tests)
内存模式(测试用)
swift
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: schema,
configurations: config
)swift
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: schema,
configurations: config
)Queries in SwiftUI
SwiftUI中的查询
Basic @Query
基础@Query
swift
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.
swift
import SwiftUI
import SwiftData
struct TracksView: View {
@Query var tracks: [Track]
var body: some View {
List(tracks) { track in
Text(track.title)
}
}
}自动更新 数据变化时视图会自动刷新。
Filtered Query
带筛选的查询
swift
struct RockTracksView: View {
@Query(filter: #Predicate<Track> { track in
track.genre == "Rock"
}) var rockTracks: [Track]
var body: some View {
List(rockTracks) { track in
Text(track.title)
}
}
}swift
struct RockTracksView: View {
@Query(filter: #Predicate<Track> { track in
track.genre == "Rock"
}) var rockTracks: [Track]
var body: some View {
List(rockTracks) { track in
Text(track.title)
}
}
}Sorted Query
带排序的查询
swift
@Query(sort: \.title, order: .forward) var tracks: [Track]
// Multiple sort descriptors
@Query(sort: [
SortDescriptor(\.artist),
SortDescriptor(\.title)
]) var tracks: [Track]swift
@Query(sort: \.title, order: .forward) var tracks: [Track]
// 多排序描述符
@Query(sort: [
SortDescriptor(\.artist),
SortDescriptor(\.title)
]) var tracks: [Track]Combined Filter + Sort
筛选+排序组合查询
swift
@Query(
filter: #Predicate<Track> { $0.duration > 180 },
sort: \.title
) var longTracks: [Track]swift
@Query(
filter: #Predicate<Track> { $0.duration > 180 },
sort: \.title
) var longTracks: [Track]ModelContext Operations
ModelContext操作
Accessing ModelContext
获取ModelContext
swift
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
func addTrack() {
let track = Track(
id: UUID().uuidString,
title: "New Song",
artist: "Artist",
duration: 240
)
modelContext.insert(track)
}
}swift
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
func addTrack() {
let track = Track(
id: UUID().uuidString,
title: "New Song",
artist: "Artist",
duration: 240
)
modelContext.insert(track)
}
}Insert
插入
swift
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// Save immediately (optional - auto-saves on view disappear)
try modelContext.save()swift
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// 立即保存(可选 - 视图消失时会自动保存)
try modelContext.save()Fetch
查询
swift
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.genre == "Rock" },
sortBy: [SortDescriptor(\.title)]
)
let rockTracks = try modelContext.fetch(descriptor)swift
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.genre == "Rock" },
sortBy: [SortDescriptor(\.title)]
)
let rockTracks = try modelContext.fetch(descriptor)Update
更新
swift
// Just modify properties — SwiftData tracks changes
track.title = "Updated Title"
// Save if needed immediately
try modelContext.save()swift
// 直接修改属性即可 — SwiftData会跟踪变化
track.title = "Updated Title"
// 如需立即保存
try modelContext.save()Delete
删除
swift
modelContext.delete(track)
try modelContext.save()swift
modelContext.delete(track)
try modelContext.save()Batch Delete
批量删除
swift
try modelContext.delete(model: Track.self, where: #Predicate { track in
track.genre == "Classical"
})swift
try modelContext.delete(model: Track.self, where: #Predicate { track in
track.genre == "Classical"
})Predicates
谓词
Basic Comparisons
基础比较
swift
#Predicate<Track> { $0.duration > 180 }
#Predicate<Track> { $0.artist == "Artist Name" }
#Predicate<Track> { $0.genre != nil }swift
#Predicate<Track> { $0.duration > 180 }
#Predicate<Track> { $0.artist == "Artist Name" }
#Predicate<Track> { $0.genre != nil }Compound Predicates
复合谓词
swift
#Predicate<Track> { track in
track.genre == "Rock" && track.duration > 180
}
#Predicate<Track> { track in
track.artist == "Artist" || track.artist == "Other Artist"
}swift
#Predicate<Track> { track in
track.genre == "Rock" && track.duration > 180
}
#Predicate<Track> { track in
track.artist == "Artist" || track.artist == "Other Artist"
}String Matching
字符串匹配
swift
// 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 ")
}swift
// 包含
#Predicate<Track> { track in
track.title.contains("Love")
}
// 不区分大小写的包含
#Predicate<Track> { track in
track.title.localizedStandardContains("love")
}
// 以指定内容开头
#Predicate<Track> { track in
track.artist.hasPrefix("The ")
}Relationship Predicates
关系谓词
swift
#Predicate<Track> { track in
track.album?.title == "Album Name"
}
#Predicate<Album> { album in
album.tracks.count > 10
}swift
#Predicate<Track> { track in
track.album?.title == "Album Name"
}
#Predicate<Album> { album in
album.tracks.count > 10
}Swift 6 Concurrency
Swift 6并发
@MainActor Isolation
@MainActor隔离
swift
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 . Use to ensure safe access from SwiftUI.
Sendable@MainActorswift
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模型不是类型。使用确保从SwiftUI访问时的安全性。
Sendable@MainActorBackground Context
后台上下文
swift
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 for background operations, not which is main-actor bound.
ModelContext(modelContainer)@Environment(\.modelContext)swift
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()
}
}模式 后台操作使用,而非绑定到主Actor的。
ModelContext(modelContainer)@Environment(\.modelContext)CloudKit Integration
CloudKit集成
Enable CloudKit Sync
启用CloudKit同步
swift
let schema = Schema([Track.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(
for: schema,
configurations: config
)swift
let schema = Schema([Track.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(
for: schema,
configurations: config
)Capabilities Required
所需能力
- Enable iCloud in Xcode (Signing & Capabilities)
- Select CloudKit
- Add iCloud container:
iCloud.com.example.MusicApp
Note SwiftData CloudKit sync is automatic - no manual conflict resolution needed.
- 在Xcode中启用iCloud(Signing & Capabilities)
- 选择CloudKit
- 添加iCloud容器:
iCloud.com.example.MusicApp
注意 SwiftData的CloudKit同步是自动的,无需手动处理冲突。
CloudKit Constraints (CRITICAL)
CloudKit约束(重要)
When using CloudKit sync, ALL properties must be optional or have default values
使用CloudKit同步时,所有属性必须是可选类型或有默认值
swift
@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
swift
@Model
final class Track {
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album? // ✅ Must be optional for CloudKit
}swift
@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仅同步到私有区域,网络延迟可能导致新记录的部分字段尚未填充。
关系约束 所有关系必须是可选类型
swift
@Model
final class Track {
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album? // ✅ 用于CloudKit时必须是可选类型
}Monitoring Sync Status (iOS 26+)
监控同步状态(iOS 26+)
swift
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var isSyncing = false
var body: some View {
VStack {
if isSyncing {
Label("Syncing with iCloud...", systemImage: "icloud.and.arrow.up.fill")
.foregroundColor(.blue)
}
List {
// Your content
}
}
.task {
// Monitor sync notifications
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
isSyncing = false
}
}
}
}swift
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var isSyncing = false
var body: some View {
VStack {
if isSyncing {
Label("正在与iCloud同步...", systemImage: "icloud.and.arrow.up.fill")
.foregroundColor(.blue)
}
List {
// 你的内容
}
}
.task {
// 监控同步通知
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
isSyncing = false
}
}
}
}Handling CloudKit Sync Conflicts
处理CloudKit同步冲突
SwiftData uses last-write-wins by default. If you need custom resolution:
swift
@MainActor
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString
var title: String = ""
var lastModified: Date = Date() // Track modification time
var deviceID: String = "" // Track which device modified
init(id: String = UUID().uuidString, title: String = "", deviceID: String) {
self.id = id
self.title = title
self.deviceID = deviceID
self.lastModified = Date()
}
}
// Conflict resolution pattern: Keep newest version
actor ConflictResolver {
let modelContext: ModelContext
init(context: ModelContext) {
self.modelContext = context
}
func resolveTrackConflict(_ local: Track, _ remote: Track) {
// Remote is newer
if remote.lastModified > local.lastModified {
local.title = remote.title
local.lastModified = remote.lastModified
local.deviceID = remote.deviceID
}
// Local is newer - keep local (do nothing)
}
}SwiftData默认使用最后写入获胜策略。如需自定义解决逻辑:
swift
@MainActor
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString
var title: String = ""
var lastModified: Date = Date() // 跟踪修改时间
var deviceID: String = "" // 跟踪修改设备
init(id: String = UUID().uuidString, title: String = "", deviceID: String) {
self.id = id
self.title = title
self.deviceID = deviceID
self.lastModified = Date()
}
}
// 冲突解决模式:保留最新版本
actor ConflictResolver {
let modelContext: ModelContext
init(context: ModelContext) {
self.modelContext = context
}
func resolveTrackConflict(_ local: Track, _ remote: Track) {
// 远程版本更新
if remote.lastModified > local.lastModified {
local.title = remote.title
local.lastModified = remote.lastModified
local.deviceID = remote.deviceID
}
// 本地版本更新 - 保留本地(无需操作)
}
}Offline Handling & Network Status
离线处理与网络状态
swift
import Network
@MainActor
class NetworkMonitor: ObservableObject {
@Published var isConnected = false
private let monitor = NWPathMonitor()
init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
}
}
monitor.start(queue: DispatchQueue.global())
}
}
struct OfflineAwareView: View {
@StateObject private var networkMonitor = NetworkMonitor()
@Query var tracks: [Track]
var body: some View {
VStack {
if !networkMonitor.isConnected {
Label("You're offline. Changes will sync when online.", systemImage: "wifi.slash")
.font(.caption)
.foregroundColor(.orange)
}
List(tracks) { track in
Text(track.title)
}
}
}
}swift
import Network
@MainActor
class NetworkMonitor: ObservableObject {
@Published var isConnected = false
private let monitor = NWPathMonitor()
init() {
monitor.pathUpdateHandler = { [weak self] path in
DispatchQueue.main.async {
self?.isConnected = path.status == .satisfied
}
}
monitor.start(queue: DispatchQueue.global())
}
}
struct OfflineAwareView: View {
@StateObject private var networkMonitor = NetworkMonitor()
@Query var tracks: [Track]
var body: some View {
VStack {
if !networkMonitor.isConnected {
Label("你当前处于离线状态,变更将在联网后同步。", systemImage: "wifi.slash")
.font(.caption)
.foregroundColor(.orange)
}
List(tracks) { track in
Text(track.title)
}
}
}
}CloudKit Record Sharing (iOS 26+)
CloudKit记录共享(iOS 26+)
swift
@MainActor
@Model
final class SharedPlaylist {
@Attribute(.unique) var id: String = UUID().uuidString
var name: String = ""
var ownerID: String = "" // CloudKit User ID of owner
@Relationship(deleteRule: .cascade, inverse: \Track.playlist)
var tracks: [Track] = []
// Share metadata
var sharedWith: [String] = [] // Array of shared user IDs
var sharePermission: SharePermission = .readOnly
init(name: String, ownerID: String) {
self.name = name
self.ownerID = ownerID
}
}
enum SharePermission: String, Codable {
case readOnly
case readWrite
}
// Share a playlist with another user
actor PlaylistSharing {
let modelContainer: ModelContainer
func sharePlaylist(_ playlist: SharedPlaylist, with userID: String) async throws {
let context = ModelContext(modelContainer)
// Add user to shared list
if !playlist.sharedWith.contains(userID) {
playlist.sharedWith.append(userID)
try context.save()
}
// Note: Actual CloudKit share URL generation requires CKShare
// This is handled by system frameworks
}
}swift
@MainActor
@Model
final class SharedPlaylist {
@Attribute(.unique) var id: String = UUID().uuidString
var name: String = ""
var ownerID: String = "" // 所有者的CloudKit用户ID
@Relationship(deleteRule: .cascade, inverse: \Track.playlist)
var tracks: [Track] = []
// 共享元数据
var sharedWith: [String] = [] // 共享用户ID数组
var sharePermission: SharePermission = .readOnly
init(name: String, ownerID: String) {
self.name = name
self.ownerID = ownerID
}
}
enum SharePermission: String, Codable {
case readOnly
case readWrite
}
// 与其他用户共享播放列表
actor PlaylistSharing {
let modelContainer: ModelContainer
func sharePlaylist(_ playlist: SharedPlaylist, with userID: String) async throws {
let context = ModelContext(modelContainer)
// 将用户添加到共享列表
if !playlist.sharedWith.contains(userID) {
playlist.sharedWith.append(userID)
try context.save()
}
// 注意:实际的CloudKit共享URL生成需要CKShare
// 这部分由系统框架处理
}
}Resolving "Property must be optional or have default value" Error
解决"Property must be optional or have default value"错误
Problem You get this error when trying to use CloudKit sync:
Property 'title' must be optional or have a default value for CloudKit synchronization问题 启用CloudKit同步时遇到以下错误:
Property 'title' must be optional or have a default value for CloudKit synchronizationSolution
解决方案
swift
// ❌ 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?
}swift
// ❌ 错误写法 - 必填属性
@Model
final class Track {
var title: String
}
// ✅ 正确写法 - 有默认值
@Model
final class Track {
var title: String = ""
}
// ✅ 另一种正确写法 - 可选类型
@Model
final class Track {
var title: String?
}Testing CloudKit Sync (Without iCloud)
测试CloudKit同步(无需iCloud)
swift
let schema = Schema([Track.self])
// Test configuration (no CloudKit sync)
let testConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: testConfig)swift
let schema = Schema([Track.self])
// 测试配置(无CloudKit同步)
let testConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: testConfig)For real CloudKit testing
真实CloudKit测试步骤
- Sign in to iCloud on test device
- Enable CloudKit in Capabilities
- Use real device (simulator CloudKit is unreliable)
- Check iCloud status in Settings → [Your Name] → iCloud
- 在测试设备上登录iCloud
- 在Capabilities中启用CloudKit
- 使用真实设备(模拟器的CloudKit不可靠)
- 在设置→[你的名字]→iCloud中检查iCloud状态
iOS 26+ Features
iOS 26+特性
Enhanced Relationship Handling
增强的关系处理
swift
@Model
final class Track {
@Relationship(
deleteRule: .cascade,
inverse: \Album.tracks,
minimum: 0,
maximum: 1 // Track belongs to at most one album
) var album: Album?
}swift
@Model
final class Track {
@Relationship(
deleteRule: .cascade,
inverse: \Album.tracks,
minimum: 0,
maximum: 1 // 一个曲目最多属于一个专辑
) var album: Album?
}Transient Properties
临时属性
swift
@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.
swift
@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)
}
}临时属性 计算属性,不会被持久化。
History Tracking
历史跟踪
swift
// Enable history tracking
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.app"),
allowsSave: true,
isHistoryEnabled: true // iOS 26+
)swift
// 启用历史跟踪
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.app"),
allowsSave: true,
isHistoryEnabled: true // iOS 26+
)Performance Patterns
性能模式
Batch Fetching
批量获取
swift
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
descriptor.fetchLimit = 100 // Paginate results
let tracks = try modelContext.fetch(descriptor)swift
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
descriptor.fetchLimit = 100 // 分页返回结果
let tracks = try modelContext.fetch(descriptor)Prefetch Relationships (Prevent N+1 Queries)
预取关系(避免N+1查询)
swift
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album] // Eager load album
let tracks = try modelContext.fetch(descriptor)
// No N+1 queries - albums already loadedCRITICAL Without prefetching, accessing in a loop triggers individual queries for EACH track:
track.album.titleswift
// ❌ 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
}swift
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album] // 预加载专辑
let tracks = try modelContext.fetch(descriptor)
// 无N+1查询 - 专辑已加载完成重要提示 若不使用预取,在循环中访问会为每个曲目触发单独的查询:
track.album.titleswift
// ❌ 缓慢: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) // 已加载完成
}Faulting (Lazy Loading)
故障延迟加载(Lazy Loading)
SwiftData uses faulting (lazy loading) by default:
swift
let track = tracks.first
// Album is a fault - not loaded yet
let albumTitle = track.album?.title
// Album loaded on access (separate query)SwiftData默认使用故障延迟加载:
swift
let track = tracks.first
// 专辑是故障对象 - 尚未加载
let albumTitle = track.album?.title
// 访问时才加载专辑(单独查询)Use faulting strategically
合理使用故障延迟加载
- ✅ Good when you access relationships in only 10-20% of cases
- ✅ Good for large relationship graphs you partially use
- ❌ Bad when you access relationships in loops → use prefetching instead
- ✅ 仅在10-20%的场景中访问关系时适用
- ✅ 适用于仅部分使用的大型关系图
- ❌ 循环中访问关系时不适用 → 改用预取
Batch Operations (Performance for Large Datasets)
批量操作(大型数据集性能优化)
swift
// ❌ 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 batchswift
// ❌ 缓慢:1000次单独保存
for track in largeDataset {
track.genre = "Updated"
try modelContext.save() // 开销大 - 执行1000次
}
// ✅ 快速:单次保存操作
for track in largeDataset {
track.genre = "Updated"
}
try modelContext.save() // 针对整个批次执行一次Index Optimization (iOS 26+)
索引优化(iOS 26+)
Create indexes on frequently queried properties:
swift
@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]为频繁查询的属性创建索引:
swift
@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]When to add indexes
何时添加索引
- ✅ Properties used in filters frequently
@Query - ✅ Properties used in sort operations
- ✅ Properties used in relationships
- ❌ NOT properties that are rarely filtered
- ❌ NOT properties that change frequently (maintenance cost)
- ✅ 频繁在筛选中使用的属性
@Query - ✅ 用于排序操作的属性
- ✅ 用于关系的属性
- ❌ 不用于很少筛选的属性
- ❌ 不用于频繁变更的属性(维护成本高)
Memory Optimization: Fetch Chunks
内存优化:分段获取
For very large datasets (100k+ records), fetch in chunks:
swift
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)])
}
}
}针对超大型数据集(10万+条记录),采用分段获取:
swift
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)])
}
}
}Avoiding Retain Cycles in CloudKit Sync
避免CloudKit同步中的循环引用
When using CloudKit, avoid capturing in closures:
selfswift
// ❌ 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()
}
}
}
}使用CloudKit时,避免在闭包中捕获:
selfswift
// ❌ 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()
}
}
}
}Common Patterns
通用模式
Search
搜索
swift
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)
}
}swift
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)
}
}Custom Sort
自定义排序
swift
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 }
}
}
}swift
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 }
}
}
}Undo/Redo
撤销/重做
swift
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
}
}swift
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.undoManager) private var undoManager
func deleteTrack(_ track: Track) {
modelContext.delete(track)
// 撤销操作由modelContext自动处理
// 使用Cmd+Z撤销
}
}Migration Strategies: From Realm & Core Data
迁移策略:从Realm和Core Data迁移
Migrating from Realm
从Realm迁移
Realm Pattern → SwiftData Equivalent
Realm模式 → SwiftData等效实现
swift
// REALM
class RealmTrack: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var artist: String
@Persisted var duration: TimeInterval
}
// SWIFTDATA
@Model
final class Track {
@Attribute(.unique) var id: String = ""
var title: String = ""
var artist: String = ""
var duration: TimeInterval = 0
init(id: String, title: String, artist: String, duration: TimeInterval) {
self.id = id
self.title = title
self.artist = artist
self.duration = duration
}
}swift
// REALM
class RealmTrack: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var artist: String
@Persisted var duration: TimeInterval
}
// SWIFTDATA
@Model
final class Track {
@Attribute(.unique) var id: String = ""
var title: String = ""
var artist: String = ""
var duration: TimeInterval = 0
init(id: String, title: String, artist: String, duration: TimeInterval) {
self.id = id
self.title = title
self.artist = artist
self.duration = duration
}
}Thread Safety Migration (Realm → SwiftData)
线程安全迁移(Realm → SwiftData)
swift
// REALM: Required explicit threading model
class RealmDataManager {
func fetchTracksOnBackground() {
DispatchQueue.global().async {
let realm = try! Realm() // Must get Realm on each thread
let tracks = realm.objects(RealmTrack.self)
DispatchQueue.main.async {
self.updateUI(tracks: Array(tracks))
}
}
}
}
// SWIFTDATA: Actor-based safety (Swift 6)
actor SwiftDataManager {
let modelContainer: ModelContainer
func fetchTracks() async -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
return try! context.fetch(descriptor)
}
}
// Usage (no manual threading needed)
@MainActor
class ViewController: UIViewController {
@State private var tracks: [Track] = []
func loadTracks() async {
tracks = await dataManager.fetchTracks()
}
}swift
// REALM:需要显式线程模型
class RealmDataManager {
func fetchTracksOnBackground() {
DispatchQueue.global().async {
let realm = try! Realm() // 必须在每个线程获取Realm实例
let tracks = realm.objects(RealmTrack.self)
DispatchQueue.main.async {
self.updateUI(tracks: Array(tracks))
}
}
}
}
// SWIFTDATA:基于Actor的安全机制(Swift 6)
actor SwiftDataManager {
let modelContainer: ModelContainer
func fetchTracks() async -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
return try! context.fetch(descriptor)
}
}
// 使用方式(无需手动处理线程)
@MainActor
class ViewController: UIViewController {
@State private var tracks: [Track] = []
func loadTracks() async {
tracks = await dataManager.fetchTracks()
}
}Relationship Migration (Realm → SwiftData)
关系迁移(Realm → SwiftData)
swift
// REALM: Explicit linking
class RealmAlbum: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var tracks: RealmSwiftCollection<RealmTrack> // Explicit collection
}
// SWIFTDATA: Inverse relationships automatic
@Model
final class Album {
@Attribute(.unique) var id: String = ""
var title: String = ""
@Relationship(deleteRule: .cascade, inverse: \Track.album)
var tracks: [Track] = []
}
@Model
final class Track {
@Attribute(.unique) var id: String = ""
var title: String = ""
var album: Album? // Inverse automatically maintained
}swift
// REALM:显式关联
class RealmAlbum: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var tracks: RealmSwiftCollection<RealmTrack> // 显式集合
}
// SWIFTDATA:自动维护反向关系
@Model
final class Album {
@Attribute(.unique) var id: String = ""
var title: String = ""
@Relationship(deleteRule: .cascade, inverse: \Track.album)
var tracks: [Track] = []
}
@Model
final class Track {
@Attribute(.unique) var id: String = ""
var title: String = ""
var album: Album? // 反向关系自动维护
}Migration Scenario: Small App (< 10,000 records)
迁移场景:小型应用(<1万条记录)
swift
actor RealmToSwiftDataMigration {
let modelContainer: ModelContainer
func migrateFromRealm(_ realmPath: String) async throws {
// 1. Read from Realm database file
let realmConfig = Realm.Configuration(fileURL: URL(fileURLWithPath: realmPath))
let realm = try await Realm(configuration: realmConfig)
// 2. Create SwiftData models
let context = ModelContext(modelContainer)
try realm.objects(RealmTrack.self).forEach { realmTrack in
let track = Track(
id: realmTrack.id,
title: realmTrack.title,
artist: realmTrack.artist,
duration: realmTrack.duration
)
context.insert(track)
}
// 3. Save to SwiftData
try context.save()
// 4. Verify migration
let descriptor = FetchDescriptor<Track>()
let tracks = try context.fetch(descriptor)
print("Migrated \(tracks.count) tracks")
}
}swift
actor RealmToSwiftDataMigration {
let modelContainer: ModelContainer
func migrateFromRealm(_ realmPath: String) async throws {
// 1. 读取Realm数据库文件
let realmConfig = Realm.Configuration(fileURL: URL(fileURLWithPath: realmPath))
let realm = try await Realm(configuration: realmConfig)
// 2. 创建SwiftData模型
let context = ModelContext(modelContainer)
try realm.objects(RealmTrack.self).forEach { realmTrack in
let track = Track(
id: realmTrack.id,
title: realmTrack.title,
artist: realmTrack.artist,
duration: realmTrack.duration
)
context.insert(track)
}
// 3. 保存到SwiftData
try context.save()
// 4. 验证迁移结果
let descriptor = FetchDescriptor<Track>()
let tracks = try context.fetch(descriptor)
print("已迁移 \(tracks.count) 条曲目")
}
}Migrating from Core Data
从Core Data迁移
Core Data Pattern → SwiftData Equivalent
Core Data模式 → SwiftData等效实现
swift
// CORE DATA
@NSManaged class CDTrack: NSManagedObject {
@NSManaged var id: String
@NSManaged var title: String
@NSManaged var duration: TimeInterval
@NSManaged var album: CDAlbum?
}
// SWIFTDATA
@Model
final class Track {
@Attribute(.unique) var id: String = ""
var title: String = ""
var duration: TimeInterval = 0
var album: Album?
}swift
// CORE DATA
@NSManaged class CDTrack: NSManagedObject {
@NSManaged var id: String
@NSManaged var title: String
@NSManaged var duration: TimeInterval
@NSManaged var album: CDAlbum?
}
// SWIFTDATA
@Model
final class Track {
@Attribute(.unique) var id: String = ""
var title: String = ""
var duration: TimeInterval = 0
var album: Album?
}Thread Confinement Migration (Core Data → SwiftData)
线程约束迁移(Core Data → SwiftData)
swift
// CORE DATA: Manual thread handling
class CoreDataManager {
var persistentContainer: NSPersistentContainer
func fetchTracks(completion: @escaping ([CDTrack]) -> Void) {
let context = persistentContainer.newBackgroundContext()
context.perform {
let request = NSFetchRequest<CDTrack>(entityName: "Track")
let results = try! context.fetch(request)
DispatchQueue.main.async {
completion(results) // ❌ Can't cross thread boundary with NSManagedObject
}
}
}
}
// SWIFTDATA: Safe async/await
class SwiftDataManager {
let modelContainer: ModelContainer
func fetchTracks() async -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
return (try? context.fetch(descriptor)) ?? []
}
}swift
// CORE DATA:手动线程处理
class CoreDataManager {
var persistentContainer: NSPersistentContainer
func fetchTracks(completion: @escaping ([CDTrack]) -> Void) {
let context = persistentContainer.newBackgroundContext()
context.perform {
let request = NSFetchRequest<CDTrack>(entityName: "Track")
let results = try! context.fetch(request)
DispatchQueue.main.async {
completion(results) // ❌ NSManagedObject无法跨线程传递
}
}
}
}
// SWIFTDATA:安全的async/await
class SwiftDataManager {
let modelContainer: ModelContainer
func fetchTracks() async -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
return (try? context.fetch(descriptor)) ?? []
}
}Batch Operations Migration (Core Data → SwiftData)
批量操作迁移(Core Data → SwiftData)
swift
// CORE DATA: Complex batch delete
class CoreDataBatchDelete {
var persistentContainer: NSPersistentContainer
func deleteOldTracks(olderThan date: Date) {
let context = persistentContainer.newBackgroundContext()
let request = NSFetchRequest<CDTrack>(entityName: "Track")
request.predicate = NSPredicate(format: "createdAt < %@", date as NSDate)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
deleteRequest.resultType = .resultTypeCount
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
print("Deleted \(result?.result ?? 0) tracks")
} catch {
print("Delete failed: \(error)")
}
}
}
// SWIFTDATA: Simple and safe
actor SwiftDataBatchDelete {
let modelContainer: ModelContainer
func deleteOldTracks(olderThan date: Date) async throws {
let context = ModelContext(modelContainer)
try context.delete(model: Track.self, where: #Predicate { track in
track.createdAt < date
})
}
}swift
// CORE DATA:复杂的批量删除
class CoreDataBatchDelete {
var persistentContainer: NSPersistentContainer
func deleteOldTracks(olderThan date: Date) {
let context = persistentContainer.newBackgroundContext()
let request = NSFetchRequest<CDTrack>(entityName: "Track")
request.predicate = NSPredicate(format: "createdAt < %@", date as NSDate)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
deleteRequest.resultType = .resultTypeCount
do {
let result = try context.execute(deleteRequest) as? NSBatchDeleteResult
print("已删除 \(result?.result ?? 0) 条曲目")
} catch {
print("删除失败:\(error)")
}
}
}
// SWIFTDATA:简单且安全
actor SwiftDataBatchDelete {
let modelContainer: ModelContainer
func deleteOldTracks(olderThan date: Date) async throws {
let context = ModelContext(modelContainer)
try context.delete(model: Track.self, where: #Predicate { track in
track.createdAt < date
})
}
}Migration Scenario: Enterprise App (Gradual Migration)
迁移场景:企业应用(逐步迁移)
swift
// Phase 1: Parallel persistence (Core Data + SwiftData)
class DualStackDataManager {
let coreDataStack: CoreDataStack
let swiftDataContainer: ModelContainer
func migrateRecord(coreDataTrack: CDTrack) async throws {
// 1. Read from Core Data
let id = coreDataTrack.id
let title = coreDataTrack.title
let artist = coreDataTrack.artist
let duration = coreDataTrack.duration
// 2. Write to SwiftData
let context = ModelContext(swiftDataContainer)
let track = Track(
id: id,
title: title,
artist: artist,
duration: duration
)
context.insert(track)
try context.save()
// 3. Mark as migrated in Core Data
coreDataTrack.isMigratedToSwiftData = true
}
// Phase 2: Cutover (mark Core Data as deprecated)
func completeMigration() {
print("Migration complete — Core Data can be removed")
}
}swift
// 阶段1:并行持久化(Core Data + SwiftData)
class DualStackDataManager {
let coreDataStack: CoreDataStack
let swiftDataContainer: ModelContainer
func migrateRecord(coreDataTrack: CDTrack) async throws {
// 1. 从Core Data读取
let id = coreDataTrack.id
let title = coreDataTrack.title
let artist = coreDataTrack.artist
let duration = coreDataTrack.duration
// 2. 写入SwiftData
let context = ModelContext(swiftDataContainer)
let track = Track(
id: id,
title: title,
artist: artist,
duration: duration
)
context.insert(track)
try context.save()
// 3. 在Core Data中标记为已迁移
coreDataTrack.isMigratedToSwiftData = true
}
// 阶段2:切换完成(标记Core Data为已废弃)
func completeMigration() {
print("迁移完成 — 可移除Core Data")
}
}CloudKit Sync Migration (Realm → SwiftData)
CloudKit同步迁移(Realm → SwiftData)
swift
// Realm uses Realm Sync (now deprecated)
// SwiftData uses CloudKit directly
@Model
final class SyncedTrack {
@Attribute(.unique) var id: String = UUID().uuidString
var title: String = ""
var syncedAt: Date = Date()
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
}
// Enable CloudKit sync in ModelConfiguration
let schema = Schema([SyncedTrack.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(for: schema, configurations: config)swift
// Realm使用Realm Sync(现已废弃)
// SwiftData直接使用CloudKit
@Model
final class SyncedTrack {
@Attribute(.unique) var id: String = UUID().uuidString
var title: String = ""
var syncedAt: Date = Date()
init(id: String = UUID().uuidString, title: String) {
self.id = id
self.title = title
}
}
// 在ModelConfiguration中启用CloudKit同步
let schema = Schema([SyncedTrack.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(for: schema, configurations: config)Testing
测试
Test Setup
测试设置
swift
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")
}
}swift
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")
}
}Comparison: SwiftData vs SQLiteData
对比:SwiftData vs SQLiteData
| Feature | SwiftData | SQLiteData |
|---|---|---|
| Type | Reference (class) | Value (struct) |
| Macro | | |
| Queries | | |
| Relationships | | Explicit foreign keys |
| CloudKit | Automatic sync | Manual SyncEngine + sharing |
| Backend | Core Data | GRDB + SQLite |
| Learning Curve | Easy (native) | Moderate |
| Performance | Good | Excellent (raw SQL) |
| 特性 | SwiftData | SQLiteData |
|---|---|---|
| 类型 | 引用类型(class) | 值类型(struct) |
| 宏 | | |
| 查询 | SwiftUI中的 | |
| 关系 | | 显式外键 |
| CloudKit | 自动同步 | 手动SyncEngine + 共享 |
| 底层实现 | Core Data | GRDB + SQLite |
| 学习曲线 | 简单(原生集成) | 中等 |
| 性能 | 良好 | 优秀(原生SQL) |
Quick Reference
快速参考
Common Operations
常用操作
swift
// Insert
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// Fetch all
@Query var tracks: [Track]
// Fetch filtered
@Query(filter: #Predicate { $0.genre == "Rock" }) var rockTracks: [Track]
// Fetch sorted
@Query(sort: \.title) var sortedTracks: [Track]
// Update
track.title = "Updated"
// Delete
modelContext.delete(track)
// Save
try modelContext.save()swift
// 插入
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// 查询所有
@Query var tracks: [Track]
// 带筛选查询
@Query(filter: #Predicate { $0.genre == "Rock" }) var rockTracks: [Track]
// 带排序查询
@Query(sort: \.title) var sortedTracks: [Track]
// 更新
track.title = "Updated"
// 删除
modelContext.delete(track)
// 保存
try modelContext.save()Resources
资源
Docs: /swiftdata
Skills: axiom-swiftdata-migration, axiom-swiftdata-migration-diag, axiom-database-migration, axiom-sqlitedata, axiom-grdb, axiom-swift-concurrency
文档:/swiftdata
相关技能:axiom-swiftdata-migration, axiom-swiftdata-migration-diag, axiom-database-migration, axiom-sqlitedata, axiom-grdb, axiom-swift-concurrency
Common Mistakes
常见错误
❌ Forgetting explicit init
❌ 忘记显式init
swift
@Model
final class Track {
var id: String
var title: String
// No init - won't compile
}Fix Always provide for classes
init@Modelswift
@Model
final class Track {
var id: String
var title: String
// 无init - 无法编译
}修复 始终为类提供
@Modelinit❌ Using structs
❌ 使用struct
swift
@Model
struct Track { } // Won't work - must be classFix Use not
final classstructswift
@Model
struct Track { } // 无法生效 - 必须是class修复 使用而非
final classstruct❌ Background operations on main context
❌ 在主上下文执行后台操作
swift
@Environment(\.modelContext) var context // Main actor only
Task {
// ❌ Crash - crossing actor boundaries
context.insert(track)
}Fix Use for background work
ModelContext(modelContainer)swift
@Environment(\.modelContext) var context // 仅主Actor可用
Task {
// ❌ 崩溃 - 跨Actor边界
context.insert(track)
}修复 后台操作使用
ModelContext(modelContainer)❌ Not saving when needed
❌ 需要时未执行保存
swift
modelContext.insert(track)
// Might not persist immediatelyFix Call for immediate persistence
try modelContext.save()Created 2025-11-28
Targets iOS 17+ (focus on iOS 26+ features)
Framework SwiftData (Apple)
Swift 5.9+ (Swift 6 concurrency patterns)
swift
modelContext.insert(track)
// 可能不会立即持久化修复 如需立即持久化,调用
try modelContext.save()创建时间 2025-11-28
目标版本 iOS 17+(聚焦iOS 26+特性)
框架 SwiftData(苹果)
Swift版本 5.9+(Swift 6并发模式)