この投稿は何?
iOSアプリケーションのデータモデルを永続化する方法について、解説する。
なお、SwiftUIとSwiftDataを利用する。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
SwiftData
SwiftDataには2つのキー・オブジェクトがある。
コンテナの役割は、モデルデータの保管場所を提供することです。
コンテナはモデルとなる型の定義を理解して、モデルデータの一貫性を維持します。
そして、コンテキストは「メモリ上のデータとコンテナとの間で調整する役」を担います。
DiceRollアプリ
SwiftUIで開発した、データ永続化を行う対象となるアプリ。
ボタンをタップすることでサイコロを振り、「出た目の数と日付」をリスト表示する。
import Foundation
struct Record {
let number: Int
let timeStamp: Date
}
import SwiftUI
struct ContentView: View {
@State private var records: [Record] = [
Record(number: 1, timeStamp: .now),
]
var body: some View {
NavigationStack {
List(records, id: \.timeStamp) { record in
RowView(record: record)
}
.navigationTitle("Records")
.safeAreaInset(edge: .bottom) {
Button {
let newRecord = Record(number: .random(in: 1...6), timeStamp: .now)
records.append(newRecord)
} label: {
Text("Dice Roll")
.padding(.horizontal)
.bold()
}
.buttonStyle(.borderedProminent)
}
}
}
}
#Preview {
ContentView()
}
import SwiftUI
struct RowView: View {
let record: Record
var body: some View {
HStack {
Image(systemName: "die.face.\(record.number)")
.resizable()
.frame(maxWidth: 44, maxHeight: 44)
.aspectRatio(1, contentMode: .fit)
Spacer()
Text(record.timeStamp, format: .dateTime.hour().minute().second().month(.wide).day())
.foregroundStyle(.secondary)
}
}
}
#Preview {
RowView(record: Record(number: 1, timeStamp: .now))
}
アプリのデータを永続化するには
DiceRollアプリにSwiftDataを導入してリストのコンテンツを永続化する作業は、以下のステップに分けられる。
- クラスをSwiftDataモデルに変換して、永続可能にする
- コンテナを作成して、アプリのストレージを設定する
- 後で使用するためのモデルをセーブする
- 表示や追加処理のためにモデルをフェッチする
なお、アプリにSwiftDataを導入するにあたっては、作業が完了するまでSwiftUIプレビューは正しく機能しないので、キャンバス上で発生するエラーは無視して良い。
クラスをSwiftDataモデルに変換して、永続可能にする
SwiftDataでモデルクラスのインスタンスを保存するには、フレームワークをインポートし、Model()
マクロでそのクラスにアノテーションをマークすることから始めます。
DiceRollアプリのデータモデルは、「サイコロの出た目と日付」を保持するRecord
構造体です。
これを基にして、SwiftDataモデルを作成します。
- Record.swift ファイルにSwiftDataフレームワークをインポートする
-
Record
型に@Model
マクロを適用する -
@Model
マクロはクラスに対してのみ有効なので、Record
型を構造体からクラスに変更する - Swiftはクラスにイニシャライザを提供しないので、イニシャライザを実装する
import Foundation
import SwiftData
@Model
class Record {
let number: Int
let timeStamp: Date
init(number: Int, timeStamp: Date) {
self.number = number
self.timeStamp = timeStamp
}
}
コンテナを作成して、アプリのストレージを設定する
SwiftDataがモデルを調べてスキーマを生成するには、事前に以下の設定を行います。
- 実行時にどのモデルを永続させるか
- 基礎となるストレージの構成(オプション)
実行時に永続するモデルは、モデルコンテナ に指定します。
- DiceRollApp.swift ファイルにSwiftDataフレームワークをインポートする
- ビュー階層のトップに、.modelContainer(for:)モディファイアでモデルコンテナを設定する
import SwiftUI
import SwiftData
@main
struct DiceRollApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Record.self)
//`Record.self`は特定のインスタンスではなく、型の定義を指す
}
}
}
プレビューの修復
プレビューとモデルコンテナを接続するが、プレビューは常に更新するたびに同じ初期状態で開始される必要がある。
.modelContainer(for:)
モディファイアのinMemory
引数にtrue
を指定すると、モデルコンテナはインメモリコンテナ方式で動作する。
つまり、アプリがメモリ上にある場合にのみ、データが保存されるようになる。
- ContentView.swift ファイルにSwiftDataフレームワークをインポートする
- プレビューにモデルコンテナを接続する
#Preview {
ContentView()
.modelContainer(for: Record.self, inMemory: true)
}
後で使用するためのモデルをセーブする
実行時にモデルクラスのインスタンスを管理するには、モデルコンテキスト を使用します。
モデルコンテキストは、「メモリ上にあるデータとモデルコンテナ間の調整役」を担うオブジェクトです。
コンテキストを取得するには、modelContext
環境変数を使用します。
すでに、アプリにはモデルコンテナが設定済みなので、ビューの環境値からモデルコンテキストを取得できます。
この方法で、「コンテナより下層のすべてのビュー」からモデルコンテキストへのアクセスが可能になります。
struct ContentView: View {
@State private var records: [Record] = [
Record(number: 1, timeStamp: .now),
]
@Environment(\.modelContext) private var context
...
}
この時点で、配列の
append(_:)
メソッドがエラー(Cannot use mutating member on immutable value: 'records' is a get-only property)を報告します。
ボタンのコードでは「新しいRecord
インスタンスを追加する」ために、append(_:)
メソッドを呼び出しています。
SwiftDataモデルのデータは、SwiftUIのコードから直接操作できません。
- 配列の追加メソッドを、モデルコンテキストの
insert(_:)
メソッドに置き換える
Button {
let newRecord = Record(number: .random(in: 1...6), timeStamp: .now)
// records.append(newRecord)
context.insert(newRecord)
} label: {
Text("Dice Roll")
.padding(.horizontal)
.bold()
}
.buttonStyle(.borderedProminent)
表示や追加処理のためにモデルをフェッチする
永続化したモデルデータに対しては、次のようなアクションが可能です。
- データを取得
- モデルインスタンスとして具体化してビューに表示
- その他のアクション
SwiftDataは、フェッチを実行するためのQuery
プロパティラッパーとFetchDescriptor
型を提供します。
-
records
プロパティの属性を@State
から@Query
に変更する -
records
プロパティの既定値を削除する
struct ContentView: View {
@Query private var records: [Record]
@Environment(\.modelContext) private var context
...
}
SwiftDataモデルの各インスタンスには、自動的にアイデンティティが提供されます。
この@Model
マクロによるアイデンティティを使用すると、リストのイニシャライザから明示的なid
指定を削除できます。
// List(records, id: \.timeStamp) { record in
List(records) { record in
RowView(record: record)
}
.navigationTitle("Records")
.safeAreaInset(edge: .bottom) {