axiom-swiftdata

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftData

SwiftData

Overview

概述

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)
这是苹果的原生持久化框架,使用
@Model
类和声明式查询。基于Core Data构建,专为SwiftUI设计。
核心原则 引用类型(
class
) +
@Model
宏 + 声明式
@Query
,实现与SwiftUI的响应式集成。
要求 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操作
  • ✅ 借助
    @Query
    实现UI自动更新
  • ✅ 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
axiom-swiftdata-migration
skill for custom schema migrations with VersionedSchema and SchemaMigrationPlan. For migration debugging, see
axiom-swiftdata-migration-diag
.
  • 需要复杂的原生SQL
  • 需要细粒度的迁移控制
迁移相关 如需自定义模式迁移(含VersionedSchema和SchemaMigrationPlan),请参考
axiom-swiftdata-migration
技能。如需迁移调试,请参考
axiom-swiftdata-migration-diag

Example 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
@Query
with predicates, sorting, and automatic view updates
→ 本技能会展示如何结合谓词、排序和自动视图更新使用
@Query

2. "When a user deletes a task list, all tasks should auto-delete too. How do I set up the relationship?"

2. "当用户删除任务列表时,所有关联任务也应自动删除,该如何设置关系?"

→ The skill explains
@Relationship
with
deleteRule: .cascade
and inverse relationships
→ 本技能会讲解带
deleteRule: .cascade
@Relationship
以及反向关系

3. "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
    final class
    , not
    struct
  • Use
    @Attribute(.unique)
    for primary key-like behavior
  • Provide explicit
    init
    (SwiftData doesn't synthesize)
  • Optional properties (
    String?
    ) are nullable
  • 使用
    final class
    ,而非
    struct
  • 使用
    @Attribute(.unique)
    实现类似主键的行为
  • 提供显式的
    init
    (SwiftData不会自动生成)
  • 可选属性(
    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 updated
swift
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 recovery
swift
// 若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

删除规则

  • .cascade
    - Delete related objects
  • .nullify
    - Set relationship to nil
  • .deny
    - Prevent deletion if relationship exists
  • .noAction
    - Leave relationship as-is (careful!)
  • .cascade
    - 删除关联对象
  • .nullify
    - 将关系设为nil
  • .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
Sendable
. Use
@MainActor
to ensure safe access from SwiftUI.
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
    }
}
原因 SwiftData模型不是
Sendable
类型。使用
@MainActor
确保从SwiftUI访问时的安全性。

Background 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
ModelContext(modelContainer)
for background operations, not
@Environment(\.modelContext)
which is main-actor bound.
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()
    }
}
模式 后台操作使用
ModelContext(modelContainer)
,而非绑定到主Actor的
@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

所需能力

  1. Enable iCloud in Xcode (Signing & Capabilities)
  2. Select CloudKit
  3. Add iCloud container:
    iCloud.com.example.MusicApp
Note SwiftData CloudKit sync is automatic - no manual conflict resolution needed.
  1. 在Xcode中启用iCloud(Signing & Capabilities)
  2. 选择CloudKit
  3. 添加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 synchronization

Solution

解决方案

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测试步骤

  1. Sign in to iCloud on test device
  2. Enable CloudKit in Capabilities
  3. Use real device (simulator CloudKit is unreliable)
  4. Check iCloud status in Settings → [Your Name] → iCloud
  1. 在测试设备上登录iCloud
  2. 在Capabilities中启用CloudKit
  3. 使用真实设备(模拟器的CloudKit不可靠)
  4. 在设置→[你的名字]→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 loaded
CRITICAL Without prefetching, accessing
track.album.title
in a loop triggers individual queries for EACH track:
swift
// ❌ 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.title
会为每个曲目触发单独的查询:
swift
// ❌ 缓慢: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 batch
swift
// ❌ 缓慢: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
    @Query
    filters frequently
  • ✅ 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
self
in closures:
swift
// ❌ 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时,避免在闭包中捕获
self
swift
// ❌ 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

FeatureSwiftDataSQLiteData
TypeReference (class)Value (struct)
Macro
@Model
@Table
Queries
@Query
in SwiftUI
@FetchAll
/
@FetchOne
Relationships
@Relationship
macro
Explicit foreign keys
CloudKitAutomatic syncManual SyncEngine + sharing
BackendCore DataGRDB + SQLite
Learning CurveEasy (native)Moderate
PerformanceGoodExcellent (raw SQL)
特性SwiftDataSQLiteData
类型引用类型(class)值类型(struct)
@Model
@Table
查询SwiftUI中的
@Query
@FetchAll
/
@FetchOne
关系
@Relationship
显式外键
CloudKit自动同步手动SyncEngine + 共享
底层实现Core DataGRDB + 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
init
for
@Model
classes
swift
@Model
final class Track {
    var id: String
    var title: String
    // 无init - 无法编译
}
修复 始终为
@Model
类提供
init

❌ Using structs

❌ 使用struct

swift
@Model
struct Track { }  // Won't work - must be class
Fix Use
final class
not
struct
swift
@Model
struct Track { }  // 无法生效 - 必须是class
修复 使用
final class
而非
struct

❌ Background operations on main context

❌ 在主上下文执行后台操作

swift
@Environment(\.modelContext) var context  // Main actor only

Task {
    // ❌ Crash - crossing actor boundaries
    context.insert(track)
}
Fix Use
ModelContext(modelContainer)
for background work
swift
@Environment(\.modelContext) var context  // 仅主Actor可用

Task {
    // ❌ 崩溃 - 跨Actor边界
    context.insert(track)
}
修复 后台操作使用
ModelContext(modelContainer)

❌ Not saving when needed

❌ 需要时未执行保存

swift
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)
swift
modelContext.insert(track)
// 可能不会立即持久化
修复 如需立即持久化,调用
try modelContext.save()

创建时间 2025-11-28 目标版本 iOS 17+(聚焦iOS 26+特性) 框架 SwiftData(苹果) Swift版本 5.9+(Swift 6并发模式)