关于 axiom-swiftdata
面向 SwiftUI 开发者的 SwiftData 技能,介绍 Apple 原生持久化框架的模型、查询与数据操作。内容还涵盖 CloudKit 同步、性能优化和并发处理。
主要功能
- 01SwiftData 模型与容器配置
- 02声明式筛选和排序查询
- 03关系与删除规则
- 04CloudKit 同步模式与约束
- 05性能及批量导入指导
- 06Swift 6 并发模式
适用场景
- 01为 SwiftUI 应用构建 CRUD 功能
- 02配置模型关系和级联删除
- 03排查 CloudKit 同步行为
- 04分页查询或导入大型数据集
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swiftdata安装后在 Claude Code 中输入/axiom-swiftdata确认已加载。
详细介绍
SwiftData
概述
Apple 原生持久化框架,使用 @Model 类和声明式查询。基于 Core Data 构建,专为 SwiftUI 设计。
核心原则 引用类型(class)+ @Model 宏 + 声明式 @Query,实现响应式 SwiftUI 集成。
要求 iOS 17+、Swift 5.9+ 目标 iOS 26+(本技能侧重于最新功能) 许可证 专有(Apple)
何时使用 SwiftData
在以下情况下选择 SwiftData
- ✅ 与 SwiftUI 原生集成
- ✅ 简单的 CRUD 操作
- ✅ 使用
@Query自动更新界面 - ✅ CloudKit 同步(iOS 17+)
- ✅ 带关系的引用类型(类)
在以下情况下改用 SQLiteData
- 需要值类型(结构体)
- 需要 CloudKit 记录共享(而不只是同步)
- 有特定性能需求的大型数据集(50k+ 条记录)
在以下情况下使用 GRDB
- 需要复杂的原始 SQL
- 需要精细控制迁移
迁移相关 有关使用 VersionedSchema 和 SchemaMigrationPlan 进行自定义架构迁移,请参阅 axiom-swiftdata-migration 技能。有关迁移调试,请参阅 axiom-swiftdata-migration-diag。
示例提示词
以下是开发者实际提出的问题,本技能旨在解答这些问题:
基本操作
1. “我有一个带文件夹的笔记应用。我需要按文件夹筛选笔记,并按最后修改时间排序。如何设置 @Query?”
→ 本技能展示如何使用带谓词和排序的 @Query,以及如何自动更新视图
2. “用户删除任务列表时,所有任务也应该自动删除。如何设置关系?”
→ 本技能讲解使用 deleteRule: .cascade 的 @Relationship 和反向关系
3. “User → Messages → Attachments 之间存在关系。删除时如何避免产生孤立数据?”
→ 本技能展示级联删除、反向关系和安全删除模式
CloudKit 与同步
4. “我的聊天应用通过 CloudKit 将消息同步到其他设备。有时消息会冲突。如何处理同步冲突?”
→ 本技能介绍 CloudKit 集成、冲突解决策略(最后写入优先、自定义解决)和同步模式
5. “我正在为应用添加 CloudKit 同步,但遇到了 ‘Property must have a default value’ 错误。哪里出了问题?”
→ 本技能讲解 CloudKit 的限制:所有属性都必须是可选的或有默认值,解释原因(网络时序),并展示修复方法
6. “我想向用户显示数据何时正在同步到 iCloud,以及他们离线时会发生什么。”
→ 本技能展示如何通过通知监控同步状态、检测网络连接,以及实现适应离线状态的界面模式
7. “我需要与其他用户共享播放列表。如何实现 CloudKit 记录共享?”
→ 本技能介绍 CloudKit 记录共享模式(iOS 26+),包括所有者/权限跟踪和共享元数据
性能与优化
8. “我需要查询 50,000 条消息,但每次只显示 20 条。如何高效分页?”
→ 本技能介绍性能模式、批量获取、限制查询,以及通过分块导入避免内存膨胀
9. “我的应用加载了 100 个带关系的任务,显示起来很慢。我怀疑是 N+1 查询。”
→ 本技能展示如何在不预取的情况下识别 N+1 问题,提供预取模式,并展示 100 倍的性能提升
10. “我正在从 API 导入 100 万条记录。怎样分批导入才能避免内存耗尽?”
→ 本技能展示基于分块的导入方式、定期保存、内存清理模式和批量操作优化
11. “哪些属性应该添加索引?我担心索引过多会拖慢写入速度。”
→ 本技能讲解索引优化模式:何时应添加索引(频繁用于筛选/排序的属性)、何时应避免添加(很少使用或频繁变化的属性),以及维护成本
从旧框架迁移
12. “我们正在从 Realm/Core Data 迁移到 SwiftData”
→ 请参阅下方“迁移”部分中的对比表,然后按照 realm-to-swiftdata-migration 或 axiom-swiftdata-migration 中的详细指南操作
@Model 定义
基本模型
import SwiftData
@Model
final class Track {
@Attribute(.unique) var id: String
var title: String
var artist: String
var duration: TimeInterval
var genre: String?
init(id: String, title: String, artist: String, duration: TimeInterval, genre: String? = nil) {
self.id = id
self.title = title
self.artist = artist
self.duration = duration
self.genre = genre
}
}
关键模式
- 使用
final class,而非struct(如果需要子类,请省略final——参见下方“类继承”) - 使用
@Attribute(.unique)实现类似主键的行为(不支持 CloudKit 同步——参见下方“CloudKit 限制”) - 提供显式
init(SwiftData 不会自动合成) - 可选属性(
String?)允许为空 - 对于即使对象被删除后仍需保留其值的属性,使用
@Attribute(.preserveValueOnDeletion)(适用于分析、审计跟踪)
关系
@Model
final class Track {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album?
init(id: String, title: String, album: Album? = nil) {
self.id = id
self.title = title
self.album = album
}
}
@Model
final class Album {
@Attribute(.unique) var id: String
var title: String
@Relationship(deleteRule: .cascade)
var tracks: [Track] = []
init(id: String, title: String) {
self.id = id
self.title = title
}
}
多对多自引用关系
@MainActor // 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
}
}
重要:SwiftData 会在你修改一侧时自动管理两侧。
✅ 正确 — 只修改一侧
// 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!
❌ 错误 — 不要手动更新两侧
user1.following.append(user2)
user2.followers.append(user1) // Redundant! Creates duplicates in CloudKit sync
取消关注(仅从一侧移除)
user1.following.removeAll { $0.id == user2.id }
try modelContext.save()
// user2.followers automatically updated
验证关系完整性(用于调试)
// Check if relationship is truly bidirectional
let user1FollowsUser2 = user1.following.contains { $0.id == user2.id }
let user2FollowedByUser1 = user2.followers.contains { $0.id == user1.id }
// These MUST always match after save()
assert(user1FollowsUser2 == user2FollowedByUser1, "Relationship corrupted!")
CloudKit 同步恢复(关系损坏时)
// If CloudKit sync creates duplicate/orphaned relationships:
// 1. Backup current state
let backup = user.following.map { $0.id }
// 2. Clear relationships
user.following.removeAll()
user.followers.removeAll()
try modelContext.save()
// 3. Rebuild from source of truth (e.g., API)
for followingId in backup {
if let followingUser = fetchUser(id: followingId) {
user.following.append(followingUser)
}
}
try modelContext.save()
// 4. Force CloudKit resync (in ModelConfiguration)
// Re-create ModelContainer to force full sync after corruption recovery
删除规则
.cascade- 删除相关对象.nullify- 将关系设为 nil.deny- 如果存在关系,则阻止删除.noAction- 保持关系不变(谨慎使用!)
类继承
SwiftData 支持用于层级模型的类继承。当存在明确的 IS-A 关系(例如,BusinessTrip IS-A Trip),且需要广泛查询(所有行程)和特定类型查询时,请使用继承。
基类和子类模式
在基类和子类上都应用 @Model。基类不要使用 final。
@Model class Trip {
@Attribute(.preserveValueOnDeletion)
var name: String
var destination: String
var startDate: Date
var endDate: Date
@Relationship(deleteRule: .cascade, inverse: \Accommodation.trip)
var accommodation: Accommodation?
init(name: String, destination: String, startDate: Date, endDate: Date) {
self.name = name
self.destination = destination
self.startDate = startDate
self.endDate = endDate
}
}
@Model class BusinessTrip: Trip {
var purpose: String
var expenseCode: String
@Relationship(deleteRule: .cascade, inverse: \BusinessMeal.trip)
var businessMeals: [BusinessMeal] = []
init(name: String, destination: String, startDate: Date, endDate: Date,
purpose: String, expenseCode: String) {
self.purpose = purpose
self.expenseCode = expenseCode
super.init(name: name, destination: destination, startDate: startDate, endDate: endDate)
}
}
使用 #Predicate 按类型查询
查询所有基类实例(包括子类),或按类型筛选:
// All trips (includes BusinessTrip, PersonalTrip, etc.)
@Query(sort: \Trip.startDate) var allTrips: [Trip]
// Only business trips — use `is` in #Predicate
@Query(filter: #Predicate<Trip> { $0 is BusinessTrip }) var businessTrips: [Trip]
// Filter on subclass-specific properties — use `as?` cast
let vacationPredicate = #Predicate<Trip> {
if let personal = $0 as? PersonalTrip {
return personal.reason == .vacation
}
return false
}
@Query(filter: vacationPredicate) var vacationTrips: [Trip]
多态关系
声明为基类类型的关系可以保存不同子类的实例:
@Model class TravelPlanner {
var name: String
@Relationship(deleteRule: .cascade)
var upcomingTrips: [Trip] = [] // Can contain BusinessTrip and PersonalTrip
init(name: String) { self.name = name }
}
进行类型转换以访问子类专有属性:
for trip in planner.upcomingTrips {
if let business = trip as? BusinessTrip {
print(business.expenseCode)
}
}
何时使用继承,何时选择替代方案
| 指标 | 使用继承 | 改用枚举/标记 |
|---|---|---|
| 子类共享许多基类属性 | 是 | — |
| 需要对所有模型进行按类型查询 | 是 | — |
| 子类有自己的关系 | 是 | — |
| 只有 1-2 个区别属性 | — | 是 |
| 仅按专用属性查询 | — | 是 |
| 遵循协议即可满足需求 | — | 是 |
保持层级结构浅(1-2 层)。深层继承链会使架构迁移和查询变得复杂。
ModelContainer 设置
SwiftUI 应用
import SwiftUI
import SwiftData
@main
struct MusicApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Track.self, Album.self])
}
}
自定义配置
let schema = Schema([Track.self, Album.self])
let config = ModelConfiguration(
schema: schema,
url: URL(fileURLWithPath: "/path/to/database.sqlite"),
cloudKitDatabase: .private("iCloud.com.example.app")
)
let container = try ModelContainer(
for: schema,
configurations: config
)
内存中(测试)
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(
for: schema,
configurations: config
)
SwiftUI 中的查询
基本 @Query
import SwiftUI
import SwiftData
struct TracksView: View {
@Query var tracks: [Track]
var body: some View {
List(tracks) { track in
Text(track.title)
}
}
}
自动更新 数据发生变化时,视图会刷新。
筛选、排序与组合
// Filtered
@Query(filter: #Predicate<Track> { $0.genre == "Rock" }) var rockTracks: [Track]
// Sorted (single)
@Query(sort: \.title, order: .forward) var tracks: [Track]
// Sorted (multiple descriptors)
@Query(sort: [SortDescriptor(\.artist), SortDescriptor(\.title)]) var tracks: [Track]
// Combined filter + sort
@Query(filter: #Predicate<Track> { $0.duration > 180 }, sort: \.title) var longTracks: [Track]
ModelContext 操作
访问 ModelContext
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
// ...
}
CRUD 操作
// Insert
let track = Track(id: "1", title: "Song", artist: "Artist", duration: 240)
modelContext.insert(track)
// Fetch
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.genre == "Rock" },
sortBy: [SortDescriptor(\.title)]
)
let rockTracks = try modelContext.fetch(descriptor)
// Update — just modify properties, SwiftData tracks changes
track.title = "Updated Title"
// Delete
modelContext.delete(track)
// Batch delete
try modelContext.delete(model: Track.self, where: #Predicate { $0.genre == "Classical" })
// Save (optional — auto-saves on view disappear)
try modelContext.save()
谓词
基本比较
#Predicate<Track> { $0.duration > 180 }
#Predicate<Track> { $0.artist == "Artist Name" }
#Predicate<Track> { $0.genre != nil }
复合谓词
#Predicate<Track> { track in
track.genre == "Rock" && track.duration > 180
}
#Predicate<Track> { track in
track.artist == "Artist" || track.artist == "Other Artist"
}
字符串匹配
// Contains
#Predicate<Track> { track in
track.title.contains("Love")
}
// Case-insensitive contains
#Predicate<Track> { track in
track.title.localizedStandardContains("love")
}
// Starts with
#Predicate<Track> { track in
track.artist.hasPrefix("The ")
}
关系谓词
#Predicate<Track> { track in
track.album?.title == "Album Name"
}
#Predicate<Album> { album in
album.tracks.count > 10
}
Swift 6 并发
@MainActor 隔离
import SwiftData
@MainActor
@Model
final class Track {
var id: String
var title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
}
原因 SwiftData 模型不是 Sendable。使用 @MainActor 确保从 SwiftUI 安全访问。
后台上下文
import SwiftData
actor DataImporter {
let modelContainer: ModelContainer
init(container: ModelContainer) {
self.modelContainer = container
}
func importTracks(_ tracks: [TrackData]) async throws {
// 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()
}
}
模式 对于后台操作,请使用 ModelContext(modelContainer),而非绑定到主 Actor 的 @Environment(\.modelContext)。
从 SwiftUI 调用
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
Button("Import") {
Task {
let importer = DataImporter(container: modelContext.container)
try await importer.importTracks(data)
}
}
}
}
CloudKit 集成
启用 CloudKit 同步
let schema = Schema([Track.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(
for: schema,
configurations: config
)
所需功能
- 在 Xcode 中启用 iCloud(Signing & Capabilities)
- 选择 CloudKit
- 添加 iCloud 容器:
iCloud.com.example.MusicApp
注意 SwiftData 的 CloudKit 同步是自动的——无需手动解决冲突。
CloudKit 限制(重要)
使用 CloudKit 同步时,所有属性都必须是可选的或具有默认值
@Model
final class Track {
var id: String = UUID().uuidString // ✅ Has default (don't use .unique — CloudKit can't enforce it)
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
}
原因 CloudKit 仅同步到私有区域,而且网络延迟意味着新记录可能尚未填充所有字段。
关系约束 所有关系都必须是可选的
@Model
final class Track {
@Relationship(deleteRule: .cascade, inverse: \Album.tracks)
var album: Album? // ✅ Must be optional for CloudKit
}
同步状态、冲突与离线处理
SwiftData CloudKit 同步默认采用最后写入胜出策略。如需了解同步状态监控、自定义冲突解决和离线感知 UI 模式,请参阅 axiom-cloud-sync。如需了解基于 CKShare 的记录共享,请参阅 axiom-cloudkit-ref。
解决“属性必须是可选的或具有默认值”错误
问题 尝试使用 CloudKit 同步时出现此错误:
Property 'title' must be optional or have a default value for CloudKit synchronization
解决方案
// ❌ 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?
}
测试 CloudKit 同步(无需 iCloud)
let schema = Schema([Track.self])
// Test configuration (no CloudKit sync)
let testConfig = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: testConfig)
进行真实的 CloudKit 测试
- 在测试设备上登录 iCloud
- 在 Capabilities 中启用 CloudKit
- 使用真机(模拟器中的 CloudKit 不可靠)
- 在“设置”→“[你的姓名]”→“iCloud”中检查 iCloud 状态
iOS 26+ 功能
改进的关系处理
@Model
final class Track {
@Relationship(
deleteRule: .cascade,
inverse: \Album.tracks,
minimum: 0,
maximum: 1 // Track belongs to at most one album
) var album: Album?
}
瞬态属性
@Model
final class Track {
var id: String
var duration: TimeInterval
@Transient
var formattedDuration: String {
let minutes = Int(duration) / 60
let seconds = Int(duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
瞬态 计算属性,不会持久化。
历史记录跟踪
// Enable history tracking
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.app"),
allowsSave: true,
isHistoryEnabled: true // iOS 26+
)
性能模式
批量获取
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
descriptor.fetchLimit = 100 // Paginate results
let tracks = try modelContext.fetch(descriptor)
预取关系(避免 N+1 查询)
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album] // Eager load album
let tracks = try modelContext.fetch(descriptor)
// No N+1 queries - albums already loaded
关键 如果不进行预取,在循环中访问 track.album.title 会为每个音轨触发单独的查询:
// ❌ SLOW: N+1 queries (1 fetch tracks + 100 fetch albums)
let tracks = try modelContext.fetch(FetchDescriptor<Track>())
for track in tracks {
print(track.album?.title) // 100 separate queries!
}
// ✅ FAST: 2 queries total (1 fetch tracks + 1 fetch all albums)
let descriptor = FetchDescriptor<Track>()
descriptor.relationshipKeyPathsForPrefetching = [\.album]
let tracks = try modelContext.fetch(descriptor)
for track in tracks {
print(track.album?.title) // Already loaded
}
故障(延迟加载)
SwiftData 默认使用故障(延迟加载):
let track = tracks.first
// Album is a fault - not loaded yet
let albumTitle = track.album?.title
// Album loaded on access (separate query)
有策略地使用故障
- ✅ 仅在 10-20% 的情况下访问关系时,适合使用
- ✅ 关系图很大但只会部分使用时,适合使用
- ❌ 在循环中访问关系时不适合 → 改用预取
批量操作(适用于大型数据集的性能优化)
// ❌ 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
索引优化(iOS 26+)
为经常查询的属性创建索引:
@Model
final class Track {
@Attribute(.unique) var id: String = UUID().uuidString
@Attribute(.indexed) // ✅ Add index
var genre: String = ""
@Attribute(.indexed)
var releaseDate: Date = Date()
var title: String = ""
var duration: TimeInterval = 0
}
// Now these queries are faster:
@Query(filter: #Predicate { $0.genre == "Rock" }) var rockTracks: [Track]
@Query(filter: #Predicate { $0.releaseDate > Date() }) var upcomingTracks: [Track]
何时添加索引
- ✅ 经常用于
@Query筛选的属性 - ✅ 用于排序操作的属性
- ✅ 用于关系的属性
- ❌ 不要为很少用于筛选的属性添加索引
- ❌ 不要为频繁变化的属性添加索引(维护成本高)
内存优化:分块获取
对于非常大的数据集(10 万条以上记录),请分块获取:
actor DataImporter {
let modelContainer: ModelContainer
func importLargeDataset(_ items: [Item]) async throws {
let chunkSize = 1000
let context = ModelContext(modelContainer)
for chunk in items.chunked(into: chunkSize) {
for item in chunk {
let track = Track(
id: item.id,
title: item.title,
artist: item.artist,
duration: item.duration
)
context.insert(track)
}
try context.save() // 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)])
}
}
}
避免 CloudKit 同步中的强引用循环
使用 CloudKit 时,避免在闭包中捕获 self:
// ❌ Retain cycle with CloudKit sync
actor TrackManager {
func startSync() {
Task {
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
self.refreshUI() // Potential retain cycle
}
}
}
}
// ✅ Proper weak capture
actor TrackManager {
func startSync() {
Task { [weak self] in
guard let self else { return }
for await notification in NotificationCenter.default
.notifications(named: NSNotification.Name("CloudKitSyncDidComplete")) {
await self.refreshUI()
}
}
}
}
常见模式
搜索
struct SearchableTracksView: View {
@Query var tracks: [Track]
@State private var searchText = ""
var filteredTracks: [Track] {
if searchText.isEmpty {
return tracks
}
return tracks.filter { track in
track.title.localizedStandardContains(searchText) ||
track.artist.localizedStandardContains(searchText)
}
}
var body: some View {
List(filteredTracks) { track in
Text(track.title)
}
.searchable(text: $searchText)
}
}
自定义排序
struct TracksView: View {
@Query var tracks: [Track]
@State private var sortOrder: SortOrder = .title
enum SortOrder {
case title, artist, duration
}
var sortedTracks: [Track] {
switch sortOrder {
case .title:
return tracks.sorted { $0.title < $1.title }
case .artist:
return tracks.sorted { $0.artist < $1.artist }
case .duration:
return tracks.sorted { $0.duration < $1.duration }
}
}
}
撤销/重做
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Environment(\.undoManager) private var undoManager
func deleteTrack(_ track: Track) {
modelContext.delete(track)
// Undo is automatic with modelContext
// Use Cmd+Z to undo
}
}
从 Realm 和 Core Data 迁移
主要区别一览
| 概念 | Realm | Core Data | SwiftData |
|---|---|---|---|
| 模型定义 | Object 子类 + @Persisted | NSManagedObject + @NSManaged | final class + @Model |
| 主键 | @Persisted(primaryKey:) | 实体检查器 | @Attribute(.unique) |
| 线程处理 | 每个线程手动创建 Realm 实例 | context.perform {} 代码块 | Actor 隔离 + ModelContext(container) |
| 关系 | RealmSwiftCollection<T> | 实体编辑器 + @NSManaged | @Relationship,自动设置反向关系 |
| 后台任务 | DispatchQueue + 线程局部 Realm | newBackgroundContext() | actor + ModelContext(modelContainer) |
| 批量删除 | 循环 + realm.delete() | NSBatchDeleteRequest | context.delete(model:where:) |
| CloudKit 同步 | Realm Sync(已于 2025 年 9 月弃用) | NSPersistentCloudKitContainer | ModelConfiguration(cloudKitDatabase:) |
详细迁移指南
realm-to-swiftdata-migration— 完整的 Realm 迁移指南:模式对应关系、线程安全转换、关系迁移、CloudKit 同步切换、时间线规划axiom-swiftdata-migration— SwiftData 架构演进:VersionedSchema、SchemaMigrationPlan、轻量级迁移与自定义迁移axiom-database-migration— 适用于任何持久化框架的安全增量迁移模式
测试
测试设置
import XCTest
import SwiftData
@testable import MusicApp
final class TrackTests: XCTestCase {
var modelContext: ModelContext!
override func setUp() async throws {
let schema = Schema([Track.self])
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: schema, configurations: config)
modelContext = ModelContext(container)
}
func testInsertTrack() throws {
let track = Track(id: "1", title: "Test", artist: "Artist", duration: 240)
modelContext.insert(track)
let descriptor = FetchDescriptor<Track>()
let tracks = try modelContext.fetch(descriptor)
XCTAssertEqual(tracks.count, 1)
XCTAssertEqual(tracks.first?.title, "Test")
}
}
对比:SwiftData 与 SQLiteData
| 特性 | SwiftData | SQLiteData |
|---|---|---|
| 类型 | 引用类型(class) | 值类型(struct) |
| 宏 | @Model | @Table |
| 查询 | SwiftUI 中的 @Query | @FetchAll / @FetchOne |
| 关系 | @Relationship 宏 | 显式外键 |
| CloudKit | 自动同步 | 手动 SyncEngine + 共享 |
| 后端 | Core Data | GRDB + SQLite |
| 学习曲线 | 简单(原生) | 适中 |
| 性能 | 良好 | 出色(原始 SQL) |
tvOS
SwiftData 在 tvOS 上没有持久化本地存储。 tvOS 没有 Documents 目录,而 Application Support 映射到 Caches——系统会在存储空间不足时删除其中的文件。仅使用本地存储的 SwiftData 存储会丢失所有数据。
tvOS SwiftData 应用必须使用 CloudKit 同步(cloudKitDatabase: .private(...))。没有 iCloud,用户数据就无法在应用启动之间保留。有关 tvOS 存储限制的完整说明,请参阅 axiom-tvos。
资源
文档:/swiftdata、/swiftdata/adopting-inheritance-in-swiftdata
Skills:axiom-swiftdata-migration、axiom-swiftdata-migration-diag、axiom-database-migration、axiom-sqlitedata、axiom-grdb、axiom-swift-concurrency
常见错误
❌ 忘记显式初始化方法
@Model
final class Track {
var id: String
var title: String
// No init - won't compile
}
修复 始终为 @Model 类提供 init
❌ 使用结构体
@Model
struct Track { } // Won't work - must be class
修复 使用 final class,而非 struct
❌ 在主上下文中执行后台操作
@Environment(\.modelContext) var context // Main actor only
Task {
// ❌ Crash - crossing actor boundaries
context.insert(track)
}
修复 在后台执行操作时使用 ModelContext(modelContainer)
❌ 需要保存时未保存
modelContext.insert(track)
// Might not persist immediately
修复 调用 try modelContext.save() 以立即持久化
创建日期 2025-11-28 目标平台 iOS 17+(重点关注 iOS 26+ 功能) 框架 SwiftData(Apple) Swift 5.9+(Swift 6 并发模式)