0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この投稿は何?

iOSアプリケーションのデータモデルを永続化する方法について、解説する。
なお、SwiftUIとSwiftDataを利用する。

Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。

SwiftData

SwiftDataには2つのキー・オブジェクトがある。

コンテナの役割は、モデルデータの保管場所を提供することです。
コンテナはモデルとなる型の定義を理解して、モデルデータの一貫性を維持します。
そして、コンテキストは「メモリ上のデータとコンテナとの間で調整する役」を担います。

DiceRollアプリ

SwiftUIで開発した、データ永続化を行う対象となるアプリ。
ボタンをタップすることでサイコロを振り、「出た目の数と日付」をリスト表示する。

スクリーンショット 2024-06-16 13.53.11.png

Record.swift
import Foundation

struct Record {
    let number: Int
    let timeStamp: Date
}
ContentView.swift
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()
}
RowView.swift
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を導入してリストのコンテンツを永続化する作業は、以下のステップに分けられる。

  1. クラスをSwiftDataモデルに変換して、永続可能にする
  2. コンテナを作成して、アプリのストレージを設定する
  3. 後で使用するためのモデルをセーブする
  4. 表示や追加処理のためにモデルをフェッチする

なお、アプリにSwiftDataを導入するにあたっては、作業が完了するまでSwiftUIプレビューは正しく機能しないので、キャンバス上で発生するエラーは無視して良い。

クラスをSwiftDataモデルに変換して、永続可能にする

SwiftDataでモデルクラスのインスタンスを保存するには、フレームワークをインポートし、Model()マクロでそのクラスにアノテーションをマークすることから始めます。

DiceRollアプリのデータモデルは、「サイコロの出た目と日付」を保持するRecord構造体です。
これを基にして、SwiftDataモデルを作成します。

  • Record.swift ファイルにSwiftDataフレームワークをインポートする
  • Record型に@Modelマクロを適用する
  • @Modelマクロはクラスに対してのみ有効なので、Record型を構造体からクラスに変更する
  • Swiftはクラスにイニシャライザを提供しないので、イニシャライザを実装する
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
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フレームワークをインポートする
  • プレビューにモデルコンテナを接続する
ContentViewのプレビュー
#Preview {
    ContentView()
        .modelContainer(for: Record.self, inMemory: true)
}

後で使用するためのモデルをセーブする

実行時にモデルクラスのインスタンスを管理するには、モデルコンテキスト を使用します。
モデルコンテキストは、「メモリ上にあるデータとモデルコンテナ間の調整役」を担うオブジェクトです。
コンテキストを取得するには、modelContext環境変数を使用します。
すでに、アプリにはモデルコンテナが設定済みなので、ビューの環境値からモデルコンテキストを取得できます。
この方法で、「コンテナより下層のすべてのビュー」からモデルコンテキストへのアクセスが可能になります。

ContentView.swift
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のコードから直接操作できません。

ボタンアクションを変更
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プロパティの既定値を削除する
ContentView.swift
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) {
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?