SwiftでiOSアプリを開発していると、アプリの更新に伴ってデータ構造を変更したくなることがあります。しかし、既存のユーザーが保存しているデータはどうなるのでしょうか?
そんな時に活躍するのがSchema.Versionです。この記事では、SwiftDataにおけるバージョン管理の仕組みを、初心者の方にもわかりやすく解説します。
Schema.Versionとは何か?
Schema.Versionは、SwiftDataでデータモデルのバージョンを管理するための仕組みです。
アプリをアップデートする際、データの構造(スキーマ)が変わることがあります。例えば:
- 新しい項目を追加したい
- 既存の項目名を変更したい
- データ型を変更したい
こうした変更を行う際、既存ユーザーのデータを壊さずに新しい構造に移行する必要があります。Schema.Versionは、この「データ移行(マイグレーション)」を安全に実行するための機能です。
なぜSchema.Versionが必要なのか?
実際の問題例
例えば、タスク管理アプリを作ったとします。最初のバージョンでは、タスクに「タイトル」と「作成日時」だけを保存していました。
// バージョン1.0のデータモデル
@Model
class Task {
var title: String
var createdAt: Date
}
しかし、ユーザーからの要望で「カテゴリー」機能を追加することになりました。
// バージョン2.0のデータモデル
@Model
class Task {
var title: String
var createdAt: Date
var category: String // 新規追加
}
この時、既存ユーザーのデータにはcategoryがありません。何も対策をしないと、アプリがクラッシュしたり、データが消えたりする可能性があります。
Schema.Versionを使えば、このような変更を安全に行えます。
Schema.Versionの基本的な使い方
ステップ1:バージョンごとのスキーマを定義する
まず、各バージョンのデータ構造をVersionedSchemaとして定義します。
import SwiftData
// バージョン1.0のスキーマ
enum TaskSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self]
}
@Model
final class Task {
var title: String
var createdAt: Date
init(title: String, createdAt: Date) {
self.title = title
self.createdAt = createdAt
}
}
}
// バージョン2.0のスキーマ
enum TaskSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self]
}
@Model
final class Task {
var title: String
var createdAt: Date
var category: String // 新しいプロパティ
init(title: String, createdAt: Date, category: String = "未分類") {
self.title = title
self.createdAt = createdAt
self.category = category
}
}
}
ステップ2:バージョン番号の意味を理解する
Schema.Version(1, 0, 0)の3つの数字には、それぞれ意味があります:
Schema.Version(major, minor, patch)
- major(メジャーバージョン): 大きな変更、互換性のない変更
- minor(マイナーバージョン): 新機能の追加、後方互換性あり
- patch(パッチバージョン): バグ修正などの小さな変更
例:
Schema.Version(1, 0, 0)→ 最初のバージョンSchema.Version(2, 0, 0)→ 大きな変更Schema.Version(2, 1, 0)→ 機能追加Schema.Version(2, 1, 1)→ バグ修正
ステップ3:マイグレーションプランを作成する
次に、どのようにデータを移行するかを定義します。
enum TaskMigrationPlan: SchemaMigrationPlan {
// すべてのスキーマバージョンを古い順に列挙
static var schemas: [any VersionedSchema.Type] {
[TaskSchemaV1.self, TaskSchemaV2.self]
}
// マイグレーションの手順を定義
static var stages: [MigrationStage] {
[migrateV1toV2]
}
// V1からV2への移行処理
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: TaskSchemaV1.self,
toVersion: TaskSchemaV2.self,
willMigrate: nil,
didMigrate: { context in
// 既存のタスクすべてに「未分類」カテゴリーを設定
let tasks = try context.fetch(FetchDescriptor<TaskSchemaV2.Task>())
for task in tasks {
if task.category.isEmpty {
task.category = "未分類"
}
}
try context.save()
}
)
}
ステップ4:ModelContainerで使用する
最後に、アプリの起動時にマイグレーションプランを指定します。
import SwiftUI
import SwiftData
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(
for: TaskSchemaV2.Task.self,
migrationPlan: TaskMigrationPlan.self
)
}
}
これで、アプリが起動すると自動的にデータ移行が実行されます。
よくある使用例
例1:新しいプロパティを追加する
最も一般的なケースです。デフォルト値を設定することで、既存データとの互換性を保ちます。
// V1: タイトルと日付のみ
@Model
final class Task {
var title: String
var createdAt: Date
}
// V2: 優先度を追加
@Model
final class Task {
var title: String
var createdAt: Date
var priority: Int = 0 // デフォルト値を設定
}
例2:プロパティ名を変更する
プロパティ名を変更する場合は、マイグレーション処理でデータをコピーします。
static let migrateV2toV3 = MigrationStage.custom(
fromVersion: TaskSchemaV2.self,
toVersion: TaskSchemaV3.self,
willMigrate: nil,
didMigrate: { context in
let tasks = try context.fetch(FetchDescriptor<TaskSchemaV3.Task>())
for task in tasks {
// 旧プロパティから新プロパティへデータをコピー
task.taskTitle = task.title
}
try context.save()
}
)
例3:複雑な変換処理
データ型を変更したり、計算が必要な場合も対応できます。
static let migrateV3toV4 = MigrationStage.custom(
fromVersion: TaskSchemaV3.self,
toVersion: TaskSchemaV4.self,
willMigrate: nil,
didMigrate: { context in
let tasks = try context.fetch(FetchDescriptor<TaskSchemaV4.Task>())
for task in tasks {
// 文字列だった優先度を数値に変換
if task.priorityString == "高" {
task.priorityLevel = 3
} else if task.priorityString == "中" {
task.priorityLevel = 2
} else {
task.priorityLevel = 1
}
}
try context.save()
}
)
Schema.Version使用時の重要なポイント
1. 一度リリースしたスキーマは変更しない
既存のVersionedSchemaは絶対に変更せず、新しいバージョンを追加してください。
// ❌ NG:既存のスキーマを変更
enum TaskSchemaV1: VersionedSchema {
// 後から修正するのはNG
}
// ✅ OK:新しいバージョンを追加
enum TaskSchemaV2: VersionedSchema {
// 変更内容を新バージョンとして定義
}
2. スキーマは古い順に並べる
schemas配列は、必ず古いバージョンから新しいバージョンの順に並べます。
static var schemas: [any VersionedSchema.Type] {
[TaskSchemaV1.self, TaskSchemaV2.self, TaskSchemaV3.self]
// V1 → V2 → V3 の順
}
3. デフォルト値を活用する
新しいプロパティには、できる限りデフォルト値を設定しましょう。
@Model
final class Task {
var title: String
var category: String = "未分類" // デフォルト値
var priority: Int = 0 // デフォルト値
}
4. テストを忘れずに
データ移行は慎重に行う必要があります。以下のテストを実施しましょう:
- 古いバージョンのデータで新バージョンを起動
- 大量データでの移行テスト
- 移行失敗時の挙動確認
マイグレーションの種類
SwiftDataでは、主に3つのマイグレーション方法があります。
1. Lightweight Migration(軽量マイグレーション)
簡単な変更の場合、SwiftDataが自動的に処理してくれます。
- 新しいプロパティの追加(デフォルト値あり)
- オプショナルプロパティの追加
- プロパティの削除
// 特別な処理は不要
static var stages: [MigrationStage] {
[.lightweight(fromVersion: TaskSchemaV1.self, toVersion: TaskSchemaV2.self)]
}
2. Custom Migration(カスタムマイグレーション)
複雑な変更には、カスタム処理を記述します。
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: TaskSchemaV1.self,
toVersion: TaskSchemaV2.self,
willMigrate: { context in
// 移行前の処理
},
didMigrate: { context in
// 移行後の処理
}
)
3. 段階的マイグレーション
複数バージョンをスキップする場合も、SwiftDataが自動的に段階的に実行します。
// V1 → V2 → V3 と段階的に実行される
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
トラブルシューティング
エラー:マイグレーションが実行されない
原因: ModelContainerにマイグレーションプランが指定されていない
解決方法:
.modelContainer(
for: Task.self,
migrationPlan: TaskMigrationPlan.self // これを忘れずに
)
エラー:データが消える
原因: スキーマの順序が間違っている、または既存スキーマを変更した
解決方法:
- schemas配列の順序を確認
- 既存スキーマは変更せず、新バージョンを追加
エラー:アプリがクラッシュする
原因: マイグレーション処理にバグがある
解決方法:
- try-catchでエラーハンドリングを追加
- ログを出力して原因を特定
didMigrate: { context in
do {
let tasks = try context.fetch(FetchDescriptor<TaskSchemaV2.Task>())
// 処理
try context.save()
} catch {
print("マイグレーションエラー: \(error)")
}
}
まとめ
Schema.Versionは、SwiftDataでデータモデルを安全に進化させるための重要な機能です。
この記事のポイント:
- Schema.Versionでデータ構造のバージョン管理ができる
- VersionedSchemaで各バージョンの構造を定義
- MigrationPlanで移行手順を指定
- 既存スキーマは変更せず、新バージョンを追加する
- デフォルト値を設定して互換性を保つ
適切にSchema.Versionを使用することで、アプリのアップデート時にユーザーのデータを守りながら、新機能を追加できます。
初めは難しく感じるかもしれませんが、基本パターンを理解すれば、安心してデータモデルを変更できるようになります。ぜひ実際のプロジェクトで試してみてください!