SwiftDataは、WWDC2023で発表されたばかりの最新のデータ永続化フレームワークです。
Xcode の新規プロジェクト作成時に生成されるテンプレートコードは、非常にシンプルな作りとなっており、SwiftDataへの一歩を踏み出す上で最適な教材であると考えています。
本稿では、現状Apple公式が準備しているドキュメントを踏まえ、適宜CoreDataと比較を行いつつ、テンプレートの解説を行います。
SwiftData概要
SwiftDataはデータ永続化フレームワークです。iOS17以降で使用することができます。
最小限のコードでアプリのモデルレイヤーを記述することができます。iCloudを通じて複数デバイス間でのデータ同期を実現します。
SwiftUIとの親和性が高く、少ないコードでデータの永続化を行うことができます。
Xcode の新規プロジェクトで保存されるテンプレートを見ることで、最小限の構成を把握することができます。
こちらを参照し、まずは主要な要素の把握を行いましょう。
SwiftDataSumpleApp.swift
このファイルは、アプリのエントリポイントになっています。
ここでは、アプリ内で共通して使用されるデータモデルの型や設定を行い、それをアプリ全体に適応しデータベース操作を行う環境の準備を行っています。
import SwiftUI
import SwiftData
@main
struct SwiftDataSumpleApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
ここでは、Schema
にItem
を設定しています。
アプリのエントリポイントでスキーマを定義することにより、アプリケーションがビルドされるときに最新のデータ構造に基づいてデータストアが設定されます。これにより、モデルクラスとデータベースの間の構造的な不整合を防ぎ、データの不整合が発生することを防いでいます。
let schema = Schema([
Item.self,
])
ModelContainer
は Schema
と ModelConfiguration
から生成されます。
modelConfiguration
にて、スキーマとストレージオプションを指定し、データモデルの設定を行っています。ここでは、永続化ストアにデータを保存する設定を行っています。
ModelContainer
を通じて、データの保存、取得、変更などを行います。
データモデルに関連する操作を行う作業領域を示す概念なので、CoreDataでいうところのNSPersistentContainer
に近い概念なのかなという印象を受けています。
sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
}
.modelContainer
修飾子を使用することで、シーンの環境にモデルコンテナと関連するモデルコンテキストを設定することができます。
アプリケーション内のすべてのビューは、この共有コンテナを介してデータ操作を行うことができ、一貫したデータ管理を実現します。
CoreDataでいうところのNSManagedObjectContext
に近い印象です。
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
ここまで見てきたコードは、以下のコードと機能としては同一です。
import SwiftUI
import SwiftData
@main
struct SDSample01App: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Item.self)
}
}
上記のコードでは、.modelContainer
の初期化時にModelContainer
を渡していました。
func modelContainer(_ container: ModelContainer) -> some Scene
.modelContainer
の初期化手法は2つあります。
サンプルコードが使用していない、永続化対象のモデル構造を渡し、よしなにコンテナを作成してもらう簡易的な手法も提供を活用すると、ここまでのコードを相当簡略化することができます。
func modelContainer(
for modelType: PersistentModel.Type,
inMemory: Bool = false,
isAutosaveEnabled: Bool = true,
isUndoEnabled: Bool = false,
onSetup: @escaping (Result<ModelContainer, Error>) -> Void = { _ in }
) -> some Scene
inMemory
はもともとfalseになっており、デフォルト設定でデータをメモリ内にのみ保存する設定になっています。
Item.swift
ここではデータ構造の定義を行なっています。
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
データベースのエンティティを設定しています。
プロパティは Date 型の timestamp のみです。
先ほど見たコードの let schema = Schema([Item.self,])
のItem
はこれを示しています。
CoreDataと比較し、非常に簡潔にデータモデルの定義が行えるようになっていますね。
@Model
マクロは、SwiftクラスをSwiftDataによって管理される保存されたモデルに変換します。
※ マクロ: コンパイル時に定型的に繰り返されるコードを生成する仕組み。
ContentView.swift
UIの見た目と簡単なデータ操作を設定しています。
ここでは、SwiftDataに関連する記述に注目します。
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
} detail: {
Text("Select an item")
}
}
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
}
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}
@Environment(\.modelContext) private var modelContext
の .modelContext
は @Environment 用の新しい Environment値です。
この宣言を通じ、modelContext
変数に、この環境内のクエリやその他のモデル操作に使用されるSwiftDataモデルコンテキストを設定できます。
@Query private var items: [Item]
により、設定されたコンテキスト内でモデルの変更が行われた際、自動的にViewの再描画の実行が果たされます。
まだ公式のドキュメントに詳細が記載されていないのですが、かなり強力な機能という印象を受けています。
データの追加を行う関数がaddItem()
です。
private func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
modelContext.insert(newItem)
}
}
先ほど@Environment
を介して取得したmodelContext
のinsert(_:)
を用いて、newItem
を追加しています。
データの削除を行う関数がdeleteItems()
です。
private func deleteItems(offsets: IndexSet) {
withAnimation {
for index in offsets {
modelContext.delete(items[index])
}
}
}
CoreDataを使用する際は、データの削除を行う際はhasChangeを確認した上でfunc save() を行いましたが、SwiftDataの際は .delete
のみで実行できるようです。
SwiftData はデータの永続化が目的ですが、プレビューを表示する部分では、Viewの内部でのみコンテナのためのモデルコンテキストを設定しています。
この設定により、プレビューでも追加削除を試すことができます。
データを保存しないので、アプリを起動する都度まっさらな状態で表示されます。
地味に便利な機能ですね。
#Preview {
ContentView()
.modelContainer(for: Item.self, inMemory: true)
}
公式のサンプルコード
次のステップに良さそうな公式のドキュメントをまとめました。
現状4つサンプルプロジェクトが提供されています。
- WWDC 2023 の「Build an app with SwiftData」セッションに対応するサンプル
- Core Data を採用しているアプリで SwiftData を採用するサンプル
- 「Migrate to SwiftData」セッション、「Dive deeper into SwiftData」と対応。
- 読み取り専用のネットワークデータをキャッシュするための永続ストアを作成および更新するサンプル。
- 一日分の地震のリストを表示し、各地震の発生時間、場所、規模を示す
- 「Bring Core Data concurrency to Swift and SwiftUI」セッションと対応
- SwiftDataによって管理されるデータを収集および変更するためのデータ入力フォームを作成するサンプルアプリ
まとめ
本稿では、SwiftDataの概要と、テンプレートコードの説明を行いました。
全体的に、CoreDataと比較し相当コードの記述量を削減できるなという印象を受けています。
@Model
と @Query
の機能の強力さも非常に感じました。
一方で、まだまだドキュメントが充実しておらず、詳細な内部挙動を把握するための試行錯誤が必要な印象も受けています。
この記事が、SwiftDataを把握する際の一助となれば幸いです。
ここまで読んでくださりありがとうございました。