| name | axiom-realm-migration-ref |
| description | Use when migrating from Realm to SwiftData - comprehensive migration guide covering pattern equivalents, threading model conversion, schema migration strategies, CloudKit sync transition, and real-world scenarios |
| license | MIT |
| metadata | {"version":"1.0.0"} |
Realm to SwiftData Migration — Reference Guide
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.
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.
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 health
Part 1: Pattern Equivalents
Model Definition Conversion
Realm → SwiftData: Basic Model
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?
}
@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
.duration duration
.genre genre
}
}
Key differences:
- Realm:
@Persisted(primaryKey: true) → SwiftData: @Attribute(.unique)
- Realm: Implicit init → SwiftData: Explicit init required
- Realm:
Object base class → SwiftData: @Model macro on final class
Realm → SwiftData: Relationships
class RealmAlbum: Object {
@Persisted(primaryKey: true) var id: String
@Persisted var title: String
@Persisted var tracks: RealmSwiftCollection<RealmTrack>
}
@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?
}
Key differences:
- Realm: Explicit
RealmSwiftCollection type → SwiftData: Native [Track] array
- Realm: Manual relationship management → SwiftData: Inverse relationships automatic
- Realm: No delete rules → SwiftData:
deleteRule: .cascade / .nullify / .deny
Realm → SwiftData: Indexes
class RealmTrack: Object {
@Persisted(primaryKey: true) var id: String
@Persisted(indexed: true) var genre: String
@Persisted(indexed: true) var releaseDate: Date
}
@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: Manual Thread Handling
class RealmDataManager {
func fetchTracksOnBackground() {
DispatchQueue.global().async {
let realm = try! 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)
}
}
}
}
Problems:
- Manual DispatchQueue threading error-prone
- Easy to access objects on wrong thread
- No compile-time guarantees
SwiftData: Actor-Based Concurrency
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()
}
}
Advantages:
- No manual DispatchQueue
- Compile-time thread safety
- Automatic actor isolation
- Swift 6 strict concurrency compatible
Common Threading Patterns
| Realm Pattern | SwiftData Pattern |
|---|
DispatchQueue.global().async | async/await in background actor |
realm.write { } | context.insert() + context.save() |
| Manual thread-local Realm instances | Shared ModelContainer + background ModelContext |
Thread.isMainThread checks | @MainActor annotations |
Part 3: Schema Migration Strategies
Simple Schema Migration (Direct Conversion)
For apps with simple schemas (< 5 tables, < 10 fields), direct migration is straightforward:
actor SchemaImporter {
let realmPath: String
let modelContainer: ModelContainer
func migrateFromRealm() async throws {
let realmConfig = Realm.Configuration(fileURL: URL(fileURLWithPath: realmPath))
let realm = try await Realm(configuration: realmConfig)
let context = ModelContext(modelContainer)
try migrateAllTracks(from: realm, to: context)
try migrateAllAlbums(from: realm, to: context)
try migrateAllPlaylists(from: realm, to: context)
try context.save()
print("Migration complete!")
}
private func migrateAllTracks(from realm: Realm, to context: ModelContext) throws {
let realmTracks = realm.objects(RealmTrack.self)
for realmTrack realmTracks {
sdTrack (
id: realmTrack.id,
title: realmTrack.title,
artist: realmTrack.artist,
duration: realmTrack.duration,
genre: realmTrack.genre
)
context.insert(sdTrack)
}
}
( : , : ) {
realmAlbums realm.objects(.)
realmAlbum realmAlbums {
sdAlbum (
id: realmAlbum.id,
title: realmAlbum.title
)
context.insert(sdAlbum)
realmTrack realmAlbum.tracks {
sdTrack findTrack(id: realmTrack.id, in: context) {
sdAlbum.tracks.append(sdTrack)
}
}
}
}
(: , : ) -> ? {
descriptor <>(
predicate: # { .id id }
)
context.fetch(descriptor).first
}
}
Complex Schema Migration (Transformation Layer)
For apps with complex schemas, many computed properties, or data transformations:
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)
}
}
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)
sdTrack (
id: dto.id,
title: dto.cleanTitle,
artist: realmTrack.artist,
duration: realmTrack.duration
)
context.insert(sdTrack)
}
context.save()
}
}
Part 4: CloudKit Sync Transition
Realm Sync → SwiftData CloudKit
Realm Sync (now deprecated) provided automatic sync. SwiftData uses CloudKit directly:
let config = Realm.Configuration(
syncConfiguration: SyncConfiguration(user: app.currentUser!)
)
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
@MainActor
class CloudKitSyncMonitor: ObservableObject {
@Published var isSyncing = false
@Published var lastSyncDate: Date?
@Published var syncError: Error?
let modelContainer: ModelContainer
func startMonitoring() {
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)
let descriptor = <>()
context.fetch(descriptor)
} {
syncError error
}
isSyncing
}
}
Migration Timing: 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
Part 5: Real-World Migration Scenarios
Scenario A: Small App (< 10,000 Records)
Timeline: 1-2 weeks
Data Size: < 10 MB
let realmPath = Realm.Configuration.defaultConfiguration.fileURL!
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)
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")
}
}
Scenario B: Medium App (100,000 - 1,000,000 Records)
Timeline: 3-4 weeks
Data Size: 100 MB - 1 GB
Challenge: Progress reporting, memory management
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)
}
context.save()
count chunk.count
onProgress(count, totalCount)
.isCancelled {
()
}
}
}
}
: {
progressView: !
statusLabel: !
() {
{
{
migrator.migrateMediumApp { current, total
.progressView.progress (current) (total)
.statusLabel.text
}
.statusLabel.text
} {
.statusLabel.text
}
}
}
}
Scenario C: Large App (Enterprise, > 1 Million Records)
Timeline: 6-8 weeks
Data Size: > 1 GB
Challenge: Minimal downtime, data integrity, rollback plan
class EnterpriseGradualMigration {
let coreDataStack: CoreDataStack
let modelContainer: ModelContainer
let batchSize = 10000
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()
.sleep(nanoseconds: )
}
() {
sdContext (modelContainer)
sdCount sdContext.fetch(<>())
realmCount countAllRealmRecords()
sdCount.count realmCount {
.countMismatch(sd: sdCount.count, realm: realmCount)
}
()
}
() {
}
}
Part 6: Testing & Verification
Data Integrity Checklist
Before going live with SwiftData:
@MainActor
class MigrationVerifier {
func verifyMigration() async throws {
print("🔍 Running migration verification...")
let sdCount = try await countSwiftDataRecords()
let realmCount = countRealmRecords()
print("✓ Record count: SD=\(sdCount), Realm=\(realmCount)")
guard sdCount == realmCount else {
throw VerificationError.countMismatch
}
try await verifySampleRecords(count: min(100, sdCount / 10))
print("✓ Spot checked 100 records - all valid")
try await verifyRelationships()
print("✓ All relationships intact")
try await verifyCloudKitSync()
print("✓ CloudKit sync working")
try await verifyPerformance()
()
()
}
(: ) {
sdContext (modelContainer)
descriptor <>()
tracks sdContext.fetch(descriptor)
sample (tracks.prefix(count))
track sample {
(track.id.isEmpty, )
(track.title.isEmpty, )
(track.duration , )
}
}
() {
sdContext (modelContainer)
albumDescriptor <>()
albums sdContext.fetch(albumDescriptor)
album albums {
track album.tracks {
(track.album.id album.id, )
}
}
}
() {
sdContext (modelContainer)
testTrack (
id: ().uuidString,
title: ,
artist: ,
duration:
)
sdContext.insert(testTrack)
sdContext.save()
()
}
() {
sdContext (modelContainer)
start ()
descriptor <>(
sortBy: [(\.title)]
)
sdContext.fetch(descriptor)
elapsed ().timeIntervalSince(start)
()
elapsed {
.performanceIssue
}
}
}
Part 7: Troubleshooting
Common Migration Issues
| Issue | Cause | Solution |
|---|
| "Property must have default" | CloudKit constraint | Add defaults: var title: String = "" |
| Relationships not synced | Missing inverse | Add inverse: \Track.album |
| 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 |
Part 8: Success Criteria
Your migration is successful when:
Quick Reference: Command Checklist
grep -r "RealmTrack\|RealmAlbum" . --include="*.swift"
let realm = try! Realm()
let count = realm.objects(RealmTrack.self).count
cp ~/Library/Developer/Realm/my_realm.realm ~/Downloads/backup.realm
// Create in-memory test container
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Track.self, configurations: config)
Settings → [Your Name] → iCloud → Check CloudKit status
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