axiom-realm-migration-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRealm to SwiftData Migration — Reference Guide
Realm 迁移至 SwiftData 参考指南
Purpose: Complete migration path from Realm to SwiftData
Swift Version: Swift 5.9+ (Swift 6 with strict concurrency recommended)
iOS Version: iOS 17+ (iOS 26+ recommended)
Context: Realm Device Sync sunset Sept 30, 2025. This guide is essential for Realm users migrating before deadline.
用途:提供从Realm到SwiftData的完整迁移路径
Swift版本:Swift 5.9+(推荐使用支持严格并发的Swift 6)
iOS版本:iOS 17+(推荐iOS 26+)
背景:Realm Device Sync 将于2025年9月30日停止服务。本指南对于在此截止日期前进行迁移的Realm用户至关重要。
Critical Timeline
关键时间线
Realm Device Sync DEPRECATION DEADLINE = September 30, 2025
If your app uses Realm Sync:
- ⚠️ You MUST migrate by September 30, 2025
- ✅ SwiftData is the recommended replacement
- ⏰ Time remaining: Depends on current date, but migrations take 2-8 weeks for production apps
This guide provides everything needed for successful migration.
Realm Device Sync 弃用截止日期 = 2025年9月30日
如果你的应用使用Realm Sync:
- ⚠️ 必须在2025年9月30日前完成迁移
- ✅ SwiftData是官方推荐的替代方案
- ⏰ 剩余时间取决于当前日期,生产应用的迁移通常需要2-8周
本指南提供了成功迁移所需的全部内容。
Migration Strategy Overview
迁移策略概述
Phase 1 (Week 1-2): Preparation & Planning
├─ Audit current Realm usage
├─ Understand model relationships
├─ Plan data migration path
└─ Set up test environment
Phase 2 (Week 2-3): Development
├─ Create SwiftData models from Realm schemas
├─ Implement data migration logic
├─ Convert threading model to async/await
└─ Test with real data
Phase 3 (Week 3-4): Migration
├─ Migrate existing app users' data
├─ Run in parallel (Realm + SwiftData)
├─ Verify CloudKit sync works
└─ Monitor for issues
Phase 4 (Week 4+): Production
├─ Deploy update with parallel persistence
├─ Gradual cutover from Realm to SwiftData
├─ Deprecate Realm code
└─ Monitor CloudKit sync healthPhase 1 (第1-2周):准备与规划
├─ 审计当前Realm使用情况
├─ 梳理模型关系
├─ 规划数据迁移路径
└─ 搭建测试环境
Phase 2 (第2-3周):开发阶段
├─ 根据Realm Schema创建SwiftData模型
├─ 实现数据迁移逻辑
├─ 将线程模型转换为async/await模式
└─ 使用真实数据测试
Phase 3 (第3-4周):迁移执行
├─ 迁移现有应用用户的数据
├─ 并行运行(Realm + SwiftData)
├─ 验证CloudKit同步功能
└─ 监控问题
Phase 4 (第4周及以后):生产上线
├─ 部署支持并行持久化的版本
├─ 逐步从Realm切换到SwiftData
├─ 弃用Realm相关代码
└─ 监控CloudKit同步健康状态Part 1: Pattern Equivalents
第一部分:模式等效转换
Model Definition Conversion
模型定义转换
Realm → SwiftData: Basic Model
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
@Persisted var genre: String?
}
// 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 differences:
- Realm: → SwiftData:
@Persisted(primaryKey: true)@Attribute(.unique) - Realm: Implicit init → SwiftData: Explicit init required
- Realm: base class → SwiftData:
Objectmacro on@Modelfinal class
swift
// REALM
class RealmTrack: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var artist: String
@Persisted var duration: TimeInterval
@Persisted var genre: String?
}
// 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
}
}核心差异:
- Realm: → SwiftData:
@Persisted(primaryKey: true)@Attribute(.unique) - Realm: 隐式初始化方法 → SwiftData: 需要显式定义初始化方法
- Realm: 继承基类 → SwiftData: 在
Object上使用final class宏@Model
Realm → SwiftData: Relationships
Realm → SwiftData:关系处理
swift
// REALM: One-to-Many
class RealmAlbum: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var tracks: RealmSwiftCollection<RealmTrack>
}
// SWIFTDATA: One-to-Many
@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
}Key differences:
- Realm: Explicit type → SwiftData: Native
RealmSwiftCollectionarray[Track] - Realm: Manual relationship management → SwiftData: Inverse relationships automatic
- Realm: No delete rules → SwiftData:
deleteRule: .cascade / .nullify / .deny
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? // 自动维护反向关系
}核心差异:
- Realm: 显式使用类型 → SwiftData: 使用原生
RealmSwiftCollection数组[Track] - Realm: 手动管理关系 → SwiftData: 自动维护反向关系
- Realm: 无删除规则 → SwiftData: 支持
deleteRule: .cascade / .nullify / .deny
Realm → SwiftData: Indexes
Realm → SwiftData:索引设置
swift
// REALM
class RealmTrack: Object {
@Persisted(primaryKey: true) var id: String
@Persisted(indexed: true) var genre: String
@Persisted(indexed: true) var releaseDate: Date
}
// SWIFTDATA
@Model
final class Track {
@Attribute(.unique) var id: String
@Attribute(.indexed) var genre: String = ""
@Attribute(.indexed) var releaseDate: Date = Date()
}swift
// REALM
class RealmTrack: Object {
@Persisted(primaryKey: true) var id: String
@Persisted(indexed: true) var genre: String
@Persisted(indexed: true) var releaseDate: Date
}
// SWIFTDATA
@Model
final class Track {
@Attribute(.unique) var id: String
@Attribute(.indexed) var genre: String = ""
@Attribute(.indexed) var releaseDate: Date = Date()
}Part 2: Threading Model Conversion
第二部分:线程模型转换
Realm Threading → Swift Concurrency
Realm线程模型 → Swift并发模型
Realm: Manual Thread Handling
Realm:手动线程处理
swift
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))
}
}
}
func saveTrackOnBackground(_ track: RealmTrack) {
DispatchQueue.global().async {
let realm = try! Realm()
try! realm.write {
realm.add(track)
}
}
}
}Problems:
- Manual DispatchQueue threading error-prone
- Easy to access objects on wrong thread
- No compile-time guarantees
swift
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))
}
}
}
func saveTrackOnBackground(_ track: RealmTrack) {
DispatchQueue.global().async {
let realm = try! Realm()
try! realm.write {
realm.add(track)
}
}
}
}存在的问题:
- 手动使用DispatchQueue容易出错
- 容易在错误的线程上访问对象
- 无编译时线程安全保证
SwiftData: Actor-Based Concurrency
SwiftData:基于Actor的并发模型
swift
actor SwiftDataManager {
let modelContainer: ModelContainer
func fetchTracks() async -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
return (try? context.fetch(descriptor)) ?? []
}
func saveTrack(_ track: Track) async {
let context = ModelContext(modelContainer)
context.insert(track)
try? context.save()
}
}
// Usage (automatic thread handling)
@MainActor
class ViewController: UIViewController {
@State private var tracks: [Track] = []
private let manager: SwiftDataManager
func loadTracks() async {
tracks = await manager.fetchTracks()
}
}Advantages:
- No manual DispatchQueue
- Compile-time thread safety
- Automatic actor isolation
- Swift 6 strict concurrency compatible
swift
actor SwiftDataManager {
let modelContainer: ModelContainer
func fetchTracks() async -> [Track] {
let context = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
return (try? context.fetch(descriptor)) ?? []
}
func saveTrack(_ track: Track) async {
let context = ModelContext(modelContainer)
context.insert(track)
try? context.save()
}
}
// 使用方式(自动线程处理)
@MainActor
class ViewController: UIViewController {
@State private var tracks: [Track] = []
private let manager: SwiftDataManager
func loadTracks() async {
tracks = await manager.fetchTracks()
}
}优势:
- 无需手动管理DispatchQueue
- 编译时线程安全保证
- 自动Actor隔离
- 兼容Swift 6严格并发模式
Common Threading Patterns
常见线程模式对比
| Realm Pattern | SwiftData Pattern |
|---|---|
| |
| |
| Manual thread-local Realm instances | Shared |
| |
| Realm模式 | SwiftData模式 |
|---|---|
| 在后台Actor中使用 |
| |
| 手动维护线程本地Realm实例 | 共享 |
| |
Part 3: Schema Migration Strategies
第三部分:Schema迁移策略
Simple Schema Migration (Direct Conversion)
简单Schema迁移(直接转换)
For apps with simple schemas (< 5 tables, < 10 fields), direct migration is straightforward:
swift
actor SchemaImporter {
let realmPath: String
let modelContainer: ModelContainer
func migrateFromRealm() async throws {
// 1. Open Realm database
let realmConfig = Realm.Configuration(fileURL: URL(fileURLWithPath: realmPath))
let realm = try await Realm(configuration: realmConfig)
// 2. Create SwiftData context
let context = ModelContext(modelContainer)
// 3. Migrate each model type
try migrateAllTracks(from: realm, to: context)
try migrateAllAlbums(from: realm, to: context)
try migrateAllPlaylists(from: realm, to: context)
// 4. Save all at once
try context.save()
print("Migration complete!")
}
private func migrateAllTracks(from realm: Realm, to context: ModelContext) throws {
let realmTracks = realm.objects(RealmTrack.self)
for realmTrack in realmTracks {
let sdTrack = Track(
id: realmTrack.id,
title: realmTrack.title,
artist: realmTrack.artist,
duration: realmTrack.duration,
genre: realmTrack.genre
)
context.insert(sdTrack)
}
}
private func migrateAllAlbums(from realm: Realm, to context: ModelContext) throws {
let realmAlbums = realm.objects(RealmAlbum.self)
for realmAlbum in realmAlbums {
let sdAlbum = Album(
id: realmAlbum.id,
title: realmAlbum.title
)
context.insert(sdAlbum)
// Connect relationships after creating all records
for realmTrack in realmAlbum.tracks {
if let sdTrack = findTrack(id: realmTrack.id, in: context) {
sdAlbum.tracks.append(sdTrack)
}
}
}
}
private func findTrack(id: String, in context: ModelContext) -> Track? {
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.id == id }
)
return try? context.fetch(descriptor).first
}
}对于Schema简单的应用(<5个表,<10个字段),直接迁移非常简单:
swift
actor SchemaImporter {
let realmPath: String
let modelContainer: ModelContainer
func migrateFromRealm() 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)
// 3. 迁移每个模型类型
try migrateAllTracks(from: realm, to: context)
try migrateAllAlbums(from: realm, to: context)
try migrateAllPlaylists(from: realm, to: context)
// 4. 一次性保存所有数据
try context.save()
print("Migration complete!")
}
private func migrateAllTracks(from realm: Realm, to context: ModelContext) throws {
let realmTracks = realm.objects(RealmTrack.self)
for realmTrack in realmTracks {
let sdTrack = Track(
id: realmTrack.id,
title: realmTrack.title,
artist: realmTrack.artist,
duration: realmTrack.duration,
genre: realmTrack.genre
)
context.insert(sdTrack)
}
}
private func migrateAllAlbums(from realm: Realm, to context: ModelContext) throws {
let realmAlbums = realm.objects(RealmAlbum.self)
for realmAlbum in realmAlbums {
let sdAlbum = Album(
id: realmAlbum.id,
title: realmAlbum.title
)
context.insert(sdAlbum)
// 创建所有记录后关联关系
for realmTrack in realmAlbum.tracks {
if let sdTrack = findTrack(id: realmTrack.id, in: context) {
sdAlbum.tracks.append(sdTrack)
}
}
}
}
private func findTrack(id: String, in context: ModelContext) -> Track? {
let descriptor = FetchDescriptor<Track>(
predicate: #Predicate { $0.id == id }
)
return try? context.fetch(descriptor).first
}
}Complex Schema Migration (Transformation Layer)
复杂Schema迁移(转换层处理)
For apps with complex schemas, many computed properties, or data transformations:
swift
// Step 1: Define transformation layer
struct TrackDTO {
let realmTrack: RealmTrack
var id: String { realmTrack.id }
var title: String { realmTrack.title }
var cleanTitle: String { realmTrack.title.trimmingCharacters(in: .whitespaces) }
var durationFormatted: String {
let minutes = Int(realmTrack.duration) / 60
let seconds = Int(realmTrack.duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
// Step 2: Migrate through transformation layer
actor ComplexMigrator {
let modelContainer: ModelContainer
func migrateWithTransformation(from realm: Realm) throws {
let context = ModelContext(modelContainer)
let realmTracks = realm.objects(RealmTrack.self)
for realmTrack in realmTracks {
let dto = TrackDTO(realmTrack: realmTrack)
// Transform data during migration
let sdTrack = Track(
id: dto.id,
title: dto.cleanTitle, // Cleaned version
artist: realmTrack.artist,
duration: realmTrack.duration
)
context.insert(sdTrack)
}
try context.save()
}
}对于Schema复杂、包含大量计算属性或需要数据转换的应用:
swift
// 步骤1:定义转换层
struct TrackDTO {
let realmTrack: RealmTrack
var id: String { realmTrack.id }
var title: String { realmTrack.title }
var cleanTitle: String { realmTrack.title.trimmingCharacters(in: .whitespaces) }
var durationFormatted: String {
let minutes = Int(realmTrack.duration) / 60
let seconds = Int(realmTrack.duration) % 60
return String(format: "%d:%02d", minutes, seconds)
}
}
// 步骤2:通过转换层迁移
actor ComplexMigrator {
let modelContainer: ModelContainer
func migrateWithTransformation(from realm: Realm) throws {
let context = ModelContext(modelContainer)
let realmTracks = realm.objects(RealmTrack.self)
for realmTrack in realmTracks {
let dto = TrackDTO(realmTrack: realmTrack)
// 迁移过程中转换数据
let sdTrack = Track(
id: dto.id,
title: dto.cleanTitle, // 清理后的标题
artist: realmTrack.artist,
duration: realmTrack.duration
)
context.insert(sdTrack)
}
try context.save()
}
}Part 4: CloudKit Sync Transition
第四部分:CloudKit同步过渡
Realm Sync → SwiftData CloudKit
Realm Sync → SwiftData CloudKit
Realm Sync (now deprecated) provided automatic sync. SwiftData uses CloudKit directly:
swift
// REALM SYNC: Automatic but deprecated
let config = Realm.Configuration(
syncConfiguration: SyncConfiguration(user: app.currentUser!)
)
// SWIFTDATA: CloudKit (recommended replacement)
let schema = Schema([Track.self, Album.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(for: schema, configurations: config)Realm Sync(已弃用)提供自动同步功能。SwiftData直接使用CloudKit:
swift
// REALM SYNC: 自动同步但已弃用
let config = Realm.Configuration(
syncConfiguration: SyncConfiguration(user: app.currentUser!)
)
// SWIFTDATA: CloudKit(推荐替代方案)
let schema = Schema([Track.self, Album.self])
let config = ModelConfiguration(
schema: schema,
cloudKitDatabase: .private("iCloud.com.example.MusicApp")
)
let container = try ModelContainer(for: schema, configurations: config)Sync Status Monitoring
同步状态监控
swift
@MainActor
class CloudKitSyncMonitor: ObservableObject {
@Published var isSyncing = false
@Published var lastSyncDate: Date?
@Published var syncError: Error?
let modelContainer: ModelContainer
func startMonitoring() {
// Monitor CloudKit sync notifications
NotificationCenter.default.addObserver(
forName: NSNotification.Name("CloudKitSyncDidComplete"),
object: nil,
queue: .main
) { [weak self] _ in
self?.isSyncing = false
self?.lastSyncDate = Date()
}
}
func syncNow() async {
isSyncing = true
do {
let context = ModelContext(modelContainer)
// SwiftData sync happens automatically
// Manually fetch to trigger sync
let descriptor = FetchDescriptor<Track>()
_ = try context.fetch(descriptor)
} catch {
syncError = error
}
isSyncing = false
}
}swift
@MainActor
class CloudKitSyncMonitor: ObservableObject {
@Published var isSyncing = false
@Published var lastSyncDate: Date?
@Published var syncError: Error?
let modelContainer: ModelContainer
func startMonitoring() {
// 监控CloudKit同步通知
NotificationCenter.default.addObserver(
forName: NSNotification.Name("CloudKitSyncDidComplete"),
object: nil,
queue: .main
) { [weak self] _ in
self?.isSyncing = false
self?.lastSyncDate = Date()
}
}
func syncNow() async {
isSyncing = true
do {
let context = ModelContext(modelContainer)
// SwiftData自动执行同步
// 手动触发查询以启动同步
let descriptor = FetchDescriptor<Track>()
_ = try context.fetch(descriptor)
} catch {
syncError = error
}
isSyncing = false
}
}Migration Timing: Realm Sync → CloudKit
迁移时间线:Realm Sync → CloudKit
Timeline:
Week 1-2: Development & Testing
├─ Create SwiftData models
├─ Test migrations in non-CloudKit mode
└─ Prepare CloudKit configuration
Week 3: CloudKit Sync Testing
├─ Enable CloudKit in test build
├─ Verify sync works with small datasets
├─ Test multi-device sync
└─ Test conflict resolution
Week 4+: Production Rollout
├─ Deploy app with SwiftData + CloudKit
├─ Initially run parallel (Realm Sync + SwiftData CloudKit)
├─ Monitor both sync mechanisms
├─ Gradually deprecate Realm Sync
└─ Final cutoff before Sept 30, 2025时间线:
第1-2周:开发与测试
├─ 创建SwiftData模型
├─ 在非CloudKit模式下测试迁移
└─ 准备CloudKit配置
第3周:CloudKit同步测试
├─ 在测试版本中启用CloudKit
├─ 验证小数据集同步功能
├─ 测试多设备同步
└─ 测试冲突解决
第4周及以后:生产部署
├─ 部署支持SwiftData + CloudKit的应用版本
├─ 初始阶段并行运行(Realm Sync + SwiftData CloudKit)
├─ 监控两种同步机制
├─ 逐步弃用Realm Sync
└─ 在2025年9月30日前完成最终切换Part 5: Real-World Migration Scenarios
第五部分:实际迁移场景
Scenario A: Small App (< 10,000 Records)
场景A:小型应用(<10,000条记录)
Timeline: 1-2 weeks
Data Size: < 10 MB
swift
// 1. Export Realm data
let realmPath = Realm.Configuration.defaultConfiguration.fileURL!
// 2. Migrate in background task
actor SmallAppMigration {
let modelContainer: ModelContainer
func migrateSmallApp() async throws {
let realmConfig = Realm.Configuration(fileURL: realmPath)
let realm = try await Realm(configuration: realmConfig)
let context = ModelContext(modelContainer)
// All-at-once migration (safe for < 10k records)
let allTracks = realm.objects(RealmTrack.self)
for realmTrack in allTracks {
let track = Track(from: realmTrack)
context.insert(track)
}
try context.save()
print("✅ Migrated \(allTracks.count) tracks")
}
}
// 3. Deploy
// Option 1: Migrate on first launch (offline)
// Option 2: Provide manual "Migrate Data" button
// Option 3: Automatic migration in background时间线:1-2周
数据量:<10 MB
swift
// 1. 导出Realm数据
let realmPath = Realm.Configuration.defaultConfiguration.fileURL!
// 2. 在后台任务中执行迁移
actor SmallAppMigration {
let modelContainer: ModelContainer
func migrateSmallApp() async throws {
let realmConfig = Realm.Configuration(fileURL: realmPath)
let realm = try await Realm(configuration: realmConfig)
let context = ModelContext(modelContainer)
// 全量迁移(适用于<10k条记录)
let allTracks = realm.objects(RealmTrack.self)
for realmTrack in allTracks {
let track = Track(from: realmTrack)
context.insert(track)
}
try context.save()
print("✅ Migrated \(allTracks.count) tracks")
}
}
// 3. 部署选项
// 选项1:首次启动时迁移(离线)
// 选项2:提供手动“迁移数据”按钮
// 选项3:在后台自动执行迁移Scenario B: Medium App (100,000 - 1,000,000 Records)
场景B:中型应用(100,000 - 1,000,000条记录)
Timeline: 3-4 weeks
Data Size: 100 MB - 1 GB
Challenge: Progress reporting, memory management
swift
actor MediumAppMigration {
let modelContainer: ModelContainer
let realmPath: String
typealias ProgressCallback = (Int, Int) -> Void
func migrateMediumApp(onProgress: @MainActor ProgressCallback) async throws {
let realmConfig = Realm.Configuration(fileURL: URL(fileURLWithPath: realmPath))
let realm = try await Realm(configuration: realmConfig)
let context = ModelContext(modelContainer)
let allTracks = realm.objects(RealmTrack.self)
let totalCount = allTracks.count
// Chunk-based migration for memory efficiency
var count = 0
for chunk in Array(allTracks).chunked(into: 5000) {
for realmTrack in chunk {
let track = Track(from: realmTrack)
context.insert(track)
}
// Save periodically
try context.save()
count += chunk.count
await onProgress(count, totalCount)
// Check for cancellation
if Task.isCancelled {
throw CancellationError()
}
}
}
}
// 4. Show progress UI
@MainActor
class MigrationViewController: UIViewController {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var statusLabel: UILabel!
func startMigration() {
Task {
do {
try await migrator.migrateMediumApp { current, total in
self.progressView.progress = Float(current) / Float(total)
self.statusLabel.text = "Migrated \(current) of \(total)..."
}
self.statusLabel.text = "✅ Migration complete!"
} catch {
self.statusLabel.text = "❌ Migration failed: \(error)"
}
}
}
}时间线:3-4周
数据量:100 MB - 1 GB
挑战:进度展示、内存管理
swift
actor MediumAppMigration {
let modelContainer: ModelContainer
let realmPath: String
typealias ProgressCallback = (Int, Int) -> Void
func migrateMediumApp(onProgress: @MainActor ProgressCallback) async throws {
let realmConfig = Realm.Configuration(fileURL: URL(fileURLWithPath: realmPath))
let realm = try await Realm(configuration: realmConfig)
let context = ModelContext(modelContainer)
let allTracks = realm.objects(RealmTrack.self)
let totalCount = allTracks.count
// 分块迁移以优化内存使用
var count = 0
for chunk in Array(allTracks).chunked(into: 5000) {
for realmTrack in chunk {
let track = Track(from: realmTrack)
context.insert(track)
}
// 定期保存
try context.save()
count += chunk.count
await onProgress(count, totalCount)
// 检查是否取消
if Task.isCancelled {
throw CancellationError()
}
}
}
}
// 4. 展示进度UI
@MainActor
class MigrationViewController: UIViewController {
@IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var statusLabel: UILabel!
func startMigration() {
Task {
do {
try await migrator.migrateMediumApp { current, total in
self.progressView.progress = Float(current) / Float(total)
self.statusLabel.text = "已迁移 \(current) / \(total) 条记录..."
}
self.statusLabel.text = "✅ 迁移完成!"
} catch {
self.statusLabel.text = "❌ 迁移失败:\(error)"
}
}
}
}Scenario C: Large App (Enterprise, > 1 Million Records)
场景C:大型应用(企业级,>1,000,000条记录)
Timeline: 6-8 weeks
Data Size: > 1 GB
Challenge: Minimal downtime, data integrity, rollback plan
swift
class EnterpriseGradualMigration {
let coreDataStack: CoreDataStack // Existing Realm
let modelContainer: ModelContainer
let batchSize = 10000
// Phase 1: Parallel migration
func startGradualMigration() async {
var offset = 0
let totalRecords = countAllRecords()
while offset < totalRecords {
let batch = fetchRealmBatch(limit: batchSize, offset: offset)
try? await migrateBatch(batch)
offset += batchSize
await reportProgress(offset, totalRecords)
}
}
private func migrateBatch(_ batch: [RealmTrack]) async throws {
let context = ModelContext(modelContainer)
for realmTrack in batch {
let track = Track(from: realmTrack)
context.insert(track)
track.migrationStatus = .completedPhase1
}
try context.save()
// Give main thread time to breathe
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
}
// Phase 2: Verify all migrated
func verifyMigrationComplete() async throws {
let sdContext = ModelContext(modelContainer)
let sdCount = try sdContext.fetch(FetchDescriptor<Track>())
let realmCount = countAllRealmRecords()
guard sdCount.count == realmCount else {
throw MigrationError.countMismatch(sd: sdCount.count, realm: realmCount)
}
print("✅ Verified: \(sdCount.count) records migrated")
}
// Phase 3: Rollback plan
func rollbackToRealm() {
// Keep Realm database intact until 100% confident
// Only delete Realm after running stable on SwiftData for 2+ weeks
}
}时间线:6-8周
数据量:>1 GB
挑战:最小化停机时间、数据完整性、回滚方案
swift
class EnterpriseGradualMigration {
let coreDataStack: CoreDataStack // 现有Realm栈
let modelContainer: ModelContainer
let batchSize = 10000
// 阶段1:并行迁移
func startGradualMigration() async {
var offset = 0
let totalRecords = countAllRecords()
while offset < totalRecords {
let batch = fetchRealmBatch(limit: batchSize, offset: offset)
try? await migrateBatch(batch)
offset += batchSize
await reportProgress(offset, totalRecords)
}
}
private func migrateBatch(_ batch: [RealmTrack]) async throws {
let context = ModelContext(modelContainer)
for realmTrack in batch {
let track = Track(from: realmTrack)
context.insert(track)
track.migrationStatus = .completedPhase1
}
try context.save()
// 给主线程留出处理时间
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
}
// 阶段2:验证全部迁移完成
func verifyMigrationComplete() async throws {
let sdContext = ModelContext(modelContainer)
let sdCount = try sdContext.fetch(FetchDescriptor<Track>())
let realmCount = countAllRealmRecords()
guard sdCount.count == realmCount else {
throw MigrationError.countMismatch(sd: sdCount.count, realm: realmCount)
}
print("✅ 验证完成:\(sdCount.count)条记录已迁移")
}
// 阶段3:回滚方案
func rollbackToRealm() {
// 在100%确认前保留Realm数据库
// 仅在SwiftData稳定运行2周后再删除Realm
}
}Part 6: Testing & Verification
第六部分:测试与验证
Data Integrity Checklist
数据完整性检查清单
Before going live with SwiftData:
swift
@MainActor
class MigrationVerifier {
func verifyMigration() async throws {
print("🔍 Running migration verification...")
// 1. Count verification
let sdCount = try await countSwiftDataRecords()
let realmCount = countRealmRecords()
print("✓ Record count: SD=\(sdCount), Realm=\(realmCount)")
guard sdCount == realmCount else {
throw VerificationError.countMismatch
}
// 2. Data integrity sampling (spot checks)
try await verifySampleRecords(count: min(100, sdCount / 10))
print("✓ Spot checked 100 records - all valid")
// 3. Relationship integrity
try await verifyRelationships()
print("✓ All relationships intact")
// 4. CloudKit sync test
try await verifyCloudKitSync()
print("✓ CloudKit sync working")
// 5. Performance test
try await verifyPerformance()
print("✓ Query performance acceptable")
print("✅ All verifications passed!")
}
private func verifySampleRecords(count: Int) async throws {
let sdContext = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
let tracks = try sdContext.fetch(descriptor)
let sample = Array(tracks.prefix(count))
for track in sample {
// Verify fields populated
assert(!track.id.isEmpty, "Track has empty ID")
assert(!track.title.isEmpty, "Track has empty title")
assert(track.duration > 0, "Track has invalid duration")
}
}
private func verifyRelationships() async throws {
let sdContext = ModelContext(modelContainer)
let albumDescriptor = FetchDescriptor<Album>()
let albums = try sdContext.fetch(albumDescriptor)
for album in albums {
// Verify inverse relationships
for track in album.tracks {
assert(track.album?.id == album.id, "Relationship broken")
}
}
}
private func verifyCloudKitSync() async throws {
let sdContext = ModelContext(modelContainer)
// Insert test record
let testTrack = Track(
id: "test-" + UUID().uuidString,
title: "Test Track",
artist: "Test Artist",
duration: 240
)
sdContext.insert(testTrack)
try sdContext.save()
// Verify CloudKit sync initiated
// (Check iCloud → iPhone → Settings → iCloud for sync status)
print("ℹ️ Check iCloud app to verify sync initiated")
}
private func verifyPerformance() async throws {
let sdContext = ModelContext(modelContainer)
let start = Date()
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
_ = try sdContext.fetch(descriptor)
let elapsed = Date().timeIntervalSince(start)
print("Fetch time: \(String(format: "%.2f", elapsed))s")
guard elapsed < 2.0 else {
throw VerificationError.performanceIssue
}
}
}在SwiftData版本上线前:
swift
@MainActor
class MigrationVerifier {
func verifyMigration() async throws {
print("🔍 执行迁移验证...")
// 1. 数量验证
let sdCount = try await countSwiftDataRecords()
let realmCount = countRealmRecords()
print("✓ 记录数量:SD=\(sdCount), Realm=\(realmCount)")
guard sdCount == realmCount else {
throw VerificationError.countMismatch
}
// 2. 数据完整性抽样(抽查)
try await verifySampleRecords(count: min(100, sdCount / 10))
print("✓ 抽查100条记录 - 全部有效")
// 3. 关系完整性
try await verifyRelationships()
print("✓ 所有关系完整")
// 4. CloudKit同步测试
try await verifyCloudKitSync()
print("✓ CloudKit同步正常")
// 5. 性能测试
try await verifyPerformance()
print("✓ 查询性能符合要求")
print("✅ 所有验证通过!")
}
private func verifySampleRecords(count: Int) async throws {
let sdContext = ModelContext(modelContainer)
let descriptor = FetchDescriptor<Track>()
let tracks = try sdContext.fetch(descriptor)
let sample = Array(tracks.prefix(count))
for track in sample {
// 验证字段已填充
assert(!track.id.isEmpty, "Track的ID为空")
assert(!track.title.isEmpty, "Track的标题为空")
assert(track.duration > 0, "Track的时长无效")
}
}
private func verifyRelationships() async throws {
let sdContext = ModelContext(modelContainer)
let albumDescriptor = FetchDescriptor<Album>()
let albums = try sdContext.fetch(albumDescriptor)
for album in albums {
// 验证反向关系
for track in album.tracks {
assert(track.album?.id == album.id, "关系断裂")
}
}
}
private func verifyCloudKitSync() async throws {
let sdContext = ModelContext(modelContainer)
// 插入测试记录
let testTrack = Track(
id: "test-" + UUID().uuidString,
title: "Test Track",
artist: "Test Artist",
duration: 240
)
sdContext.insert(testTrack)
try sdContext.save()
// 验证CloudKit同步已启动
// (前往iCloud → iPhone → 设置 → iCloud查看同步状态)
print("ℹ️ 请在iCloud应用中验证同步已启动")
}
private func verifyPerformance() async throws {
let sdContext = ModelContext(modelContainer)
let start = Date()
let descriptor = FetchDescriptor<Track>(
sortBy: [SortDescriptor(\.title)]
)
_ = try sdContext.fetch(descriptor)
let elapsed = Date().timeIntervalSince(start)
print("查询耗时:\(String(format: "%.2f", elapsed))s")
guard elapsed < 2.0 else {
throw VerificationError.performanceIssue
}
}
}Part 7: Troubleshooting
第七部分:故障排除
Common Migration Issues
常见迁移问题
| Issue | Cause | Solution |
|---|---|---|
| "Property must have default" | CloudKit constraint | Add defaults: |
| Relationships not synced | Missing inverse | Add |
| Sync stuck | CloudKit auth issue | Check Settings → iCloud → CloudKit |
| Memory bloat during import | No chunking | Implement batch import (1000 at a time) |
| Data loss | No backup | Keep Realm copy for 2 weeks post-migration |
| 问题 | 原因 | 解决方案 |
|---|---|---|
| "Property must have default" | CloudKit约束 | 添加默认值: |
| 关系未同步 | 缺少反向关系定义 | 添加 |
| 同步卡住 | CloudKit授权问题 | 检查设置 → iCloud → CloudKit |
| 导入时内存膨胀 | 未分块 | 实现批量导入(每次1000条) |
| 数据丢失 | 未备份 | 迁移后保留Realm副本2周 |
Part 8: Success Criteria
第八部分:成功标准
Your migration is successful when:
- All data migrated correctly (count matches)
- Sample record verification passes (spot checks 100+ records)
- Relationships intact (inverse relationships work)
- CloudKit sync enabled and working
- Performance acceptable (queries < 1 second)
- No data races (Swift 6 strict concurrency)
- Tested on real device (not just simulator)
- Rollback plan documented and tested
- Realm database kept as backup for 2 weeks
- Zero crashes in production after 1 week
当满足以下条件时,迁移视为成功:
- 所有数据迁移正确(数量匹配)
- 抽样记录验证通过(抽查100+条记录)
- 关系完整(反向关系正常工作)
- CloudKit同步已启用且正常运行
- 性能符合要求(查询耗时<1秒)
- 无数据竞争(兼容Swift 6严格并发)
- 在真实设备上测试(而非仅模拟器)
- 回滚方案已文档化并测试
- Realm数据库作为备份保留2周
- 上线1周后生产环境无崩溃
Quick Reference: Command Checklist
快速参考:命令清单
bash
undefinedbash
undefined1. Audit Realm usage
1. 审计Realm使用情况
grep -r "RealmTrack|RealmAlbum" . --include="*.swift"
grep -r "RealmTrack|RealmAlbum" . --include="*.swift"
2. Count Realm records (in app)
2. 统计Realm记录数(在应用内执行)
let realm = try! Realm()
let count = realm.objects(RealmTrack.self).count
let realm = try! Realm()
let count = realm.objects(RealmTrack.self).count
3. Export Realm database
3. 导出Realm数据库
cp ~/Library/Developer/Realm/my_realm.realm ~/Downloads/backup.realm
cp ~/Library/Developer/Realm/my_realm.realm ~/Downloads/backup.realm
4. Test SwiftData models
4. 测试SwiftData模型
// Create in-memory test container
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Track.self, configurations: config)
// 创建内存测试容器
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Track.self, configurations: config)
5. Verify CloudKit
5. 验证CloudKit
Settings → [Your Name] → iCloud → Check CloudKit status
---设置 → [你的名称] → iCloud → 检查CloudKit状态
---Resources
资源
WWDC: 2024-10137
Docs: /swiftdata
Skills: axiom-swiftdata, axiom-swift-concurrency, axiom-database-migration
Created: 2025-11-30
Status: Production-ready migration guide
Urgency: Realm Device Sync sunset September 30, 2025
Estimated Migration Time: 2-8 weeks depending on app complexity
WWDC:2024-10137
文档:/swiftdata
技能:axiom-swiftdata, axiom-swift-concurrency, axiom-database-migration
创建时间:2025-11-30
状态:可用于生产环境的迁移指南
紧急程度:Realm Device Sync 将于2025年9月30日停止服务
预估迁移时间:2-8周,取决于应用复杂度