はじめに
こんにちは アドベントカレンダー11日目担当のsyoです。 特にこだわりがあるわけではないのですが結果的にApple製品を多く持っているので、Appleが作成したプログラミング言語であるSwiftに興味があり、WWDC2023でSwiftDataなるものが発表されたのでそれについて調べてみました。
SwiftDataとはデータ永続化の新しいフレームワーク
まずは公式の引用
SwiftDataは、アプリ内のデータを管理するためのまったく新しいフレームワークです。通常のSwiftコードを使ってモデルを記述でき、カスタムエディタは必要ありません。SwiftDataでは、関係管理、元に戻す/やり直しのサポート、iCloudとの同期などが自動的に行われます。また、SwiftDataはSwiftUIと統合されているため、データがすぐに反映され、ビューは常に最新の状態に保たれます。
引用の繰り返しになりますが、
- Swiftコードでデータモデルの記述ができる
- SwiftUIと統合され、簡単にデータの反映ができる
がポイントだと思います。
SwiftDataのテンプレートコードを読んでいく
実行の様子
まずはイメージとして実行の様子を見てみましょう。 テンプレートコードでは、ボタンを押した瞬間のタイムスタンプのデータを管理します。タイムスタンプのデータをSwiftDataで永続化して管理しており、追加と削除が可能です。
- 右上の「+」を押すと、ボタンを押したタイミングのタイムスタンプが追加されます。
- スワイプや「Edit」を押すことでタイムスタンプの削除ができます。
ソースコード
次にソースコードを見てみましょう。このコードはStorageにSwiftDataを選択して新規プロジェクトを作成すると生成されます。(方法は に書きました。)また、この節の下部に全てのコードを載せたので、必要に応じて見てみてください。
まずは初めに挙げた二つのポイント
- Swiftコードでデータモデルの記述ができる
- SwiftUIと統合され、簡単にデータの反映ができる
についてみていきます。
1. Swiftコードでデータモデルの記述ができる
import Foundation
import SwiftData
@Model //@Modelをつけることによってclassをスキーマとして扱うことができる
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
ここではclassとしてItemが宣言され、そのItemクラスがプロパティにtimestampを持っています。普通のclassに@Modelをつけるだけでデータモデルの定義ができます。簡潔でわかりやすいですね。
2. SwiftUIと統合され、簡単にデータの反映ができる
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)
}
//省略
ここで注目して欲しいのは@Query private var items: [Item]の宣言部分とForEach(items) { item inから始まる利用部分です。この宣言をするだけでItemテーブル(?)のデータをitemsに格納してくれ、それを通常通りに利用して表示することができます。また、このコードのままでデータの更新があった際も表示に反映してくれます。
次に、データの追加・消去についてみていきます。
import SwiftUI
import SwiftData
struct ContentView: View {
//注目箇所
@Environment(\.modelContext) private var modelContext
@Query private var items: [Item]
var body: some View {
//省略
}
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はCRUD処理を担うobjectです。
このmodelContextを用いてmodelContext.insert(newItem)やmodelContext.delete(items[index])という関数を用いてデータの追加と削除が行われています。
初めの宣言の@Environmentは環境変数(?)を取得する際に利用するもので、タイムゾーンなどの取得ができます。このmodelContextはどこからくるんだというと、どうやら最後の.modelContainer(for: Item.self, inMemory: true)の時に設定されているようです。(https://developer.apple.com/documentation/SwiftUI/View/modelContainer(_:)を参照)これはPreviewようの部分ですが、ビルドした時場合に実行される部分でも多少表記が違いますが、同様のコードが見られます。
import SwiftUI
import SwiftData
@main
struct SwiftDataTestApp: 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)
}
}
全てのコード
最後に全てのコードを載せておきます。
- データモデルに関わるコード(Item.swift)
import Foundation import SwiftData @Model final class Item { var timestamp: Date init(timestamp: Date) { self.timestamp = timestamp } }
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)
}
#if os(macOS)
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
#endif
.toolbar {
#if os(iOS)
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
#endif
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)
}
import SwiftUI
import SwiftData
@main
struct SwiftDataTestApp: 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)
}
}
SwiftDataに関わるキーワード
ここまでで一応テンプレートのコードにざっと目を通したのですが、まだまだ分からないことだらけです。かといって長々と書いても読みにくい(そもそも書けない)ので必要になるであろうキーワードを書くことにします。
実はSwiftDataに登場している者たち(すみません、よく分かってないです。)
- Schema:データモデルを指定して初期化
- ModelConfiguration:Schemaを使って初期化
- ModelContainer :schemaやModelConfigurationを使って初期化
- ModelContext:ModelContainerを利用して初期化
CRUD処理を担う
データモデルの定義関係
- マクロ:コンパイル時のコード変換
エラー表示も行われ、使いやすい…らしい。SwiftDataが簡単に実装できる(ように見える)のはマクロの恩恵が大きい#で始まる自立型マクロと@で始まる付属型マクロがある。(しかし、#や@がついているからと言ってマクロとは限らないみたい…)
- @Attribute(.unique):ユニーク制約をつけるマクロ
@Attribute(.unique) var name: Stringのように宣言することでこのプロパティに対してユニーク制約をつけることができる。
- @Relationship:リレーションを定義するマクロ
@Relationship(.cascade) var accommodation: Accommodation?のように宣言することでリレーションを定義できる。もちろん1対1、1対多、多対多のリレーションも定義できる。なお、.cascadeをつけることによって、データを削除した時に関係のあるデータの削除も行なってくれるらしい。
- @Transient:永続化しない
@Transient var destinationWeather = Weather.current()のように初期値とともに宣言することによってそのプロパティは永続化しない扱いにできる。
CRUD処理関係
- ModelContext.save():変更の保存
- ModelContext.insert(_:):データの追加
一度insertを行ったら、そのインスタンスを直接変更することでupdateの処理も可能らしい
- ModelContext.delete(_:):データの削除
- ModelContext.fetch(_:):データの取得
FetchDescriptorを引数に取り、これを指定することによって検索の条件やソートの方法の指定が可能
SwiftDataで何かしようと思った場合に出てくる単語だと思うので、公式ドキュメントや先人たちを頼りに調べてみてください。
まとめ
Swiftはちょっと知らない間に大きく変わっていたりするのでちゃんと触れていないと分からなくなってしまうところがあります…
今回SwiftDataという新しいフレームワークに触れてみましたが、簡潔さの中にはマクロをはじめとして、別の新しい概念や隠されている部分も多くあり、これが果たして使いやすいものであるのかどうかはまだまだ分かりません。
個人的にはSwiftのコードでデータモデルを定義できるのは見やすくて良いなと感じたので、もう少し調べていきたいと思います。
参考
正直なところまだまだ情報が少なく、かつ私自身Swiftの知識も全然足りていません。先駆者の記事に大変お世話になりました。
- 公式ドキュメント
- https://developer.apple.com/documentation/swiftdata
正直分からないことだらけですが、公式の情報を片手に他のものを読んでいくのが良いと思います。
- SwiftData入門
- https://zenn.dev/yumemi_inc/articles/2a929c839b2000
SwiftDataの挙動について詳しく書かれており、とても助けられました。本当に素晴らしいです。
- SwiftDataめっちゃええやん。
- https://qiita.com/Ri__/items/0c46c432b2e873ceeb04
最低限必要となるCRUD機能を簡潔にまとめてくださっていました。初めのイメージを掴むのに適していると思います。
おまけ SwiftDataの新規プロジェクトの作成
- Xcodeを開き、「Create New Project…」を選択
- 今回は「Multiplatform>App」を選択
- 「Product Name」を入力し、「Storage」で「SwiftData」を選択
- 完成(右側のPreviewはiPhoneにしました。)