mcpField
A
axiom-swiftdata

axiom-swiftdata

github.com/charleswiltgen数据与数据库1.2k294 次安装今天更新
代码仓库

关于 axiom-swiftdata

面向 SwiftUI 开发者的 SwiftData 技能,介绍 Apple 原生持久化框架的模型、查询与数据操作。内容还涵盖 CloudKit 同步、性能优化和并发处理。

主要功能

  1. 01SwiftData 模型与容器配置
  2. 02声明式筛选和排序查询
  3. 03关系与删除规则
  4. 04CloudKit 同步模式与约束
  5. 05性能及批量导入指导
  6. 06Swift 6 并发模式

适用场景

  1. 01为 SwiftUI 应用构建 CRUD 功能
  2. 02配置模型关系和级联删除
  3. 03排查 CloudKit 同步行为
  4. 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
)
所需功能
  1. 在 Xcode 中启用 iCloud(Signing & Capabilities)
  2. 选择 CloudKit
  3. 添加 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 测试
  1. 在测试设备上登录 iCloud
  2. 在 Capabilities 中启用 CloudKit
  3. 使用真机(模拟器中的 CloudKit 不可靠)
  4. 在“设置”→“[你的姓名]”→“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 迁移

主要区别一览
概念RealmCore DataSwiftData
模型定义Object 子类 + @PersistedNSManagedObject + @NSManagedfinal class + @Model
主键@Persisted(primaryKey:)实体检查器@Attribute(.unique)
线程处理每个线程手动创建 Realm 实例context.perform {} 代码块Actor 隔离 + ModelContext(container)
关系RealmSwiftCollection<T>实体编辑器 + @NSManaged@Relationship,自动设置反向关系
后台任务DispatchQueue + 线程局部 RealmnewBackgroundContext()actor + ModelContext(modelContainer)
批量删除循环 + realm.delete()NSBatchDeleteRequestcontext.delete(model:where:)
CloudKit 同步Realm Sync(已于 2025 年 9 月弃用)NSPersistentCloudKitContainerModelConfiguration(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

特性SwiftDataSQLiteData
类型引用类型(class)值类型(struct)
宏@Model@Table
查询SwiftUI 中的 @Query@FetchAll / @FetchOne
关系@Relationship 宏显式外键
CloudKit自动同步手动 SyncEngine + 共享
后端Core DataGRDB + 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 并发模式)

评论

支持 Markdown