WWDC23で発表されたSwiftDataについて、実際にプログラムを書いて試してみました。
SwiftDataとは
SwiftData makes it easy to persist data using declarative code. You can query and filter data using regular Swift code. And it’s designed to integrate seamlessly with SwiftUI.
データの永続化、SwiftUIとのシームレスな統合。いわばSwiftUIと親和性のあるCoreData, RealmSwiftみたいなものでしょうか。
データ定義
@Model
class Pokemon {
@Attribute(.unique) var name: String
var iconURL: URL
var types: [String]
var abilities: [String]
init(name: String, iconURL: URL, types: [String], abilities: [String]) {
self.name = name
self.iconURL = iconURL
self.types = types
self.abilities = abilities
}
}
データを定義しているクラスに、@Model
をつけます。これだけでそのクラスはSwiftDataによって永続化が可能になります。
さらに、@Model
をつけることでそのデータ定義クラスはObservable
プロトコルに準拠します。
Observableについて詳しくは以下のリンクやWWDCのセッションを見ると良さそうです。SwiftDataにおいてはデータ変化をUIに通知する、ObservableObjectの代わりに用いられるもの、と言えるかもしれません。
また、@Attribute(.unique)
でユニーク制約を表現できたりします。
SwiftUIで使ってみる
定義したデータはView側でこのように使えたりします。
import SwiftUI
import SwiftData
struct PokeListView: View {
@Query private var pokeArray: [Pokemon]
@State private var showingAdditionView = false
var body: some View {
NavigationView {
List(pokeArray) { pokemon in
HStack {
AsyncImage(url: pokemon.iconURL) {
$0.image?.resizable()
}
.frame(width: 50, height: 50)
.padding(.trailing)
VStack(alignment: .leading) {
Text("なまえ: \(pokemon.name)")
Text("タイプ: \(pokemon.types.joined(separator: "・"))")
Text("とくせい: \(pokemon.abilities.joined(separator: "・"))")
}
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("ついか") {
self.showingAdditionView.toggle()
}
}
}
.sheet(isPresented: $showingAdditionView) {
PokeAdditionView()
}
}
}
}
@Query
という新規のproperty wrapperを追加します。これによりSwiftUI ViewからSwiftDataで定義したモデルのデータを取得することができます。
PokemonクラスがObservableプロトコルに準拠している、そして@Query
を使用していることで@State
のようにデータの変化に合わせてViewが更新されるようにもなっています。
さて、永続化する処理はどのようにして呼び出すのでしょう。
このプログラムでは、ポケモンの名前やタイプなどを入力すると保存ができるようにしたいです。
import SwiftUI
import SwiftData
struct PokeAdditionView: View {
@Environment(\.presentationMode) private var presentationMode
@Environment(\.modelContext) private var modelContext
@ObservedObject private var viewItem: PokeAdditionViewItem = PokeAdditionViewItem()
var body: some View {
VStack {
PokeSimpleTextFiled(text: $viewItem.name, title: "なまえ")
PokeSimpleTextFiled(text: $viewItem.iconURLText, title: "アイコン")
PokeMultipleTextFiled(textArray: $viewItem.typeArray, title: "タイプ")
PokeMultipleTextFiled(textArray: $viewItem.abilityArray, title: "とくせい")
Button(action: {
let pokemon = Pokemon(
name: viewItem.name,
iconURL: URL(string: viewItem.iconURLText)!,
types: viewItem.typeArray,
abilities: viewItem.abilityArray
)
self.modelContext.insert(pokemon)
self.presentationMode.wrappedValue.dismiss()
}) {
Text("ほぞん")
}.disabled(!viewItem.saveButtonEnabled)
Spacer()
}
.padding()
}
}
モデルデータの追加は、self.modelContext.insert(pokemon)
の呼び出しで行っています。
このmodelContextとはなんでしょうか。
ModelContext
An object that enables you to fetch, insert, and delete models, and save any changes to disk.
いわばデータソースとして扱うためのオブジェクトです。
データの追加には、insertメソッドを使うようです。> func insert<T>(object: T)
他にも、fetch、update、deleteなどのメソッドがあります。
ModelContextを使ってデータを操作していくのはなんとなく掴めました。
そしてもう一点、ModelContextを扱うにあたって必要な要素があります。ModelContainer
です。
ModelContainer
An object that manages an app’s schema and model storage configuration.
「アプリのスキーマとモデルストレージの設定を管理するオブジェクト」とのこと。アプリ全体のストレージスタックを生成するようです。
おそらくこのオブジェクトの設定がないとデータを使用したいViewなどでデータ操作ができません。
そのため説明動画に沿って、ModelContainerをセットアップしてみます。
import SwiftUI
@main
struct practice_swiftdataApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Pokemon.self)
}
}
modelContainer(for: Pokemon.self)
← このモディファイアが必要です。
メソッド定義を見る感じ、for引数に複数のモデル型を配列でセットできるイニシャライザもある模様。
動かしてみる
ポケモンの名前を登録して、リストに出すだけのプログラムを作成しました。
SwiftDataが正しく使えていれば、アプリを一度閉じても登録したポケモンの表示が出るはず。
何も登録してない状態から追加して、アプリを閉じる。そして再度開く。
出ました。大したことをしていないので全てが参考になるわけでは無いですが、Realmなどよりも楽に使えそうなポテンシャルを感じます。
(TCAと使うときどうするんだろう。。)