MENU

【Swift初心者向け】Schema.Versionとは?SwiftDataでデータ移行を安全に行う方法

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でデータモデルを安全に進化させるための重要な機能です。

この記事のポイント:

  1. Schema.Versionでデータ構造のバージョン管理ができる
  2. VersionedSchemaで各バージョンの構造を定義
  3. MigrationPlanで移行手順を指定
  4. 既存スキーマは変更せず、新バージョンを追加する
  5. デフォルト値を設定して互換性を保つ

適切にSchema.Versionを使用することで、アプリのアップデート時にユーザーのデータを守りながら、新機能を追加できます。

初めは難しく感じるかもしれませんが、基本パターンを理解すれば、安心してデータモデルを変更できるようになります。ぜひ実際のプロジェクトで試してみてください!

参考リソース


プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次