Most apps ship updates like this:
UserDefaults.standard.set(true, forKey: "newFlag")
That worksβ¦
until you need:
- database schema changes
- new required fields
- offline data preservation
- sync queue compatibility
- multi-version clients in the wild
- rollback safety
- tenant-specific migrations
At that point, data migration becomes one of the riskiest parts of your app.
This post shows how to design a migration system in SwiftUI that is:
- safe
- deterministic
- backward-compatible
- testable
- production-grade
π§ The Core Principle
App versions change β user data must survive.
Your schema is a contract with every version youβve ever shipped.
π§± 1. Version Your Data Schema
Never rely on implicit structure.
struct SchemaVersion: Codable {
let version: Int
}
Persist current version:
let currentSchemaVersion = 3
𧬠2. Migration Engine Lives in Infrastructure
final class MigrationEngine {
private let persistence: PersistenceLayer
private let migrations: [MigrationStep]
}
It does not live in:
- Views
- ViewModels
- feature modules
Migration is cross-cutting infrastructure.
π 3. Define Migration Steps
struct MigrationStep {
let from: Int
let to: Int
let migrate: () throws -> Void
}
Example:
MigrationStep(from: 1, to: 2) {
addField("lastOpenedAt", default: Date())
}
Migrations must be:
- ordered
- idempotent
- atomic
π¦ 4. Run Migrations at App Launch
func performMigrations() throws {
let storedVersion = loadVersion()
for step in migrations where step.from == storedVersion {
try step.migrate()
saveVersion(step.to)
}
}
Run before:
- sync engine starts
- ViewModels load data
- background tasks resume
β οΈ 5. Backward Compatibility Matters
Users may skip versions.
Migration path must support:
v1 β v4
Not only:
v3 β v4
π 6. Migration & Sync Queue Compatibility
If schema changes:
- queued operations may become invalid
- payload formats may change
Strategy:
migrateQueuedOperations()
Never drop user actions silently.
π 7. Safe Rollback Strategy
App updates fail in the real world.
Rules:
- never delete old columns immediately
- support dual-read during transition
- remove fields only after several versions
This prevents catastrophic data loss.
π§ͺ 8. Testing Migrations
Test scenarios:
- upgrade from every prior version
- partial migrations
- corrupted data
- interrupted migrations
Migration bugs are permanent.
β οΈ 9. Common Migration Anti-Patterns
Avoid:
- destructive schema changes
- silent data resets
- skipping version tracking
- running migrations lazily
- mixing migration with feature logic
These lead to:
- data loss
- sync failures
- inconsistent state
π§ Mental Model
Think:
Old Schema
β Migration Engine
β New Schema
β Sync Engine
β App Features
Not:
βWeβll just reset the database.β
π Final Thoughts
A proper migration system gives you:
- safe app upgrades
- preserved user data
- compatibility with old client
- fewer production incidents
- confidence to evolve your schema
This is the difference between:
- a fragile app
- and a long-lived product
Top comments (0)