CoreDataからSwiftDataへデータの引き継ぎ
はじめに
現在、CoreDataを利用したアプリをリリースしています。
SwiftDataの発表を受け、移行作業を進めています。
その際 CoreData版で保存したデータがSwiftData版に引き継がれない という問題に直面しました。
その解決方法の一つをこの記事に記します。
前提
Adopting SwiftData for a Core Data app
この記事では上記のドキュメントおよびサンプルコードを基本として話を展開します。
Appleから提供されたコードではデータの引き継ぎができなかったため、サンプルコードに手を加える形で解決方法を説明します。
結論
CoreData版でデータを保存した場所のURLを指定することで、SwiftData版にデータ引き継ぎすることができます。
import SwiftUI
import SwiftData
@main
struct TripsApp: App {
let container = {
let url = URL.applicationSupportDirectory.appendingPathComponent("Trips.sqlite")
let configuration = ModelConfiguration(url: url)
return try! ModelContainer(for: Trip.self, configurations: configuration)
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
TripsApp(サンプルコード)について
Appleのサンプルコードとして、3つのXcodeプロジェクトが提供されています。
BundleIDは同じなので、全て同じアプリとして認識されます。
- Trips-CoreData - CoreDataのみを利用するプロジェクト
- Trips-Coexistence - アプリ本体ではCoreData、ウィジェットではSwiftDataを利用するプロジェクト
- Trips-SwiftData - SwiftDataのみを利用するプロジェクト
旅行の計画 Trip
を作成して確認することができる、というアプリです。
動作確認
下記の手順でアプリの起動、データの作成を行います。
- Trips-CoreData プロジェクトを実行
-
Trip
を作成 - Trips-SwiftData プロジェクトを実行
- 2で作成した
Trip
を確認
1はアプリの新規インストール、3は新しいバージョンへのアップデート、と考えることができます。
この時、4では Trip
が表示されることを期待しますが、実際は何も表示されません。
CoreData版で保存したデータがSwiftData版に引き継がれていない、ということです。
どうすれば4で Trips
が表示されるのか、を考えていきます。
SwiftDataではどこにデータが保存されるのか
Additionally, the app sets up the container using ModelContainer to ensure that all views access the same ModelContainer.
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self)
}
}
ドキュメント * およびサンプルコードでは、ModelContainer
を設定することで全ての画面から同じデータにアクセスすることができる、と記載されています
では、そのコンテナはどこにあるのか?どこを見ているのか?という疑問が生まれます。
ModelContainer
* の中身を見ると、
public class ModelContainer : Equatable, @unchecked Sendable {
final public let schema: Schema
final public let migrationPlan: (SchemaMigrationPlan.Type)?
public var configurations: Set<ModelConfiguration>
// ...
}
さらに、怪しそうな ModelConfiguration
* の中身を見ると、
public struct ModelConfiguration : Identifiable, Hashable {
public let url: URL
public let name: String
// ...
}
となっています。
ModelConfiguration.url
が関係していそうです。
デフォルトでどのようなURLが設定されるのか気になるので print
で確認してみます。
let modelConfiguration = ModelConfiguration()
let url = modelConfiguration.url
print(url)
出力結果:
file:///.../Containers/Data/Application/.../Library/Application%20Support/default.store
どうやら default.store
というファイルにデータが保存されるようです。
CoreDataではどこにデータが保存されるのか
CoreData版サンプルコードでは NSPersistentContainer
からURLを取得することができます。
let persistentStoreDescription = NSPersistentContainer(name: "Trips").persistentStoreDescriptions.first!
let url = persistentStoreDescription.url!
print(url)
出力結果:
file://.../Containers/Data/Application/.../Library/Application%20Support/Trips.sqlite
Trips.sqlite
というファイルに保存されていることが分かります。
特別な実装をしていなければ、その他の多くのアプリでも同じ場所に保存されているはずです。
この場所の取得方法は下記の通り。
let url = URL.applicationSupportDirectory.appendingPathComponent("Trips.sqlite")
解決方法
結論としては default.store
ではなく Trips.sqlite
に保存するよう指定すれば良い、ということになります。
SwiftData版サンプルコードでは modelContainer(for:inMemory:isAutosaveEnabled:isUndoEnabled:onSetup:)
* を使って、間接的にコンテナを設定しています。
@main
struct TripsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Trip.self)
}
}
今回の場合、URLを指定したいのでコンテナを自作して modelContainer(_:)
* から直接コンテナを設定します。
@main
struct TripsApp: App {
let container = {
let url = URL.applicationSupportDirectory.appendingPathComponent("Trips.sqlite")
let configuration = ModelConfiguration(url: url)
return try! ModelContainer(for: Trip.self, configurations: configuration)
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
SwiftData版サンプルコードへの変更は以上です。
下記の手順でアプリの起動、データの作成を行います。
- Trips-CoreData プロジェクトを実行
Trip
を作成- Trips-SwiftData プロジェクトを実行
- 2で作成した
Trip
を確認
3のプロジェクトに手を加えた状態で同様の手順を試すと、4で Trip
が表示されることを確認できるはずです。
English Title
Migrating Data from CoreData to SwiftData
Discussion