【SwiftData】起動時にアプリのモデルデータを保存する方法

# この投稿は何？
Apple開発者向けドキュメント「[Preserving your app’s model data across launches](https://developer.apple.com/documentation/swiftdata/preserving-your-apps-model-data-across-launches)」を読む。

Swiftマクロを使用して、クラスをSwiftDataモデルに変換します。
そして、アプリを再起動してもモデルのインスタンスが消えないようにします。

> Swiftを基礎から学ぶには
自著、工学社より発売中の「[まるごと分かるSwiftプログラミング](https://amzn.asia/d/7YPWJwl)」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
> また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「[今日からはじめるプログラミング](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwii1qTuj_2BAxV4hlYBHbj5BqEQFnoECBYQAQ&url=https%3A%2F%2Fwww.udemy.com%2Fcourse%2Fstart-programming-today%2F&usg=AOvVaw2vs4Sccq-bpfA4ZD2rCyNm&opi=89978449)」をお勧めします。

## Overview

ほとんどのアプリは、作成または消費するデータをモデル化するいくつかの独自型を定義しています。
例えば、旅行アプリでは旅行、フライト、および予約された宿泊施設のクラスを定義かもしれません。
SwiftDataフレームワークはデータを迅速かつ効率的に保有して、アプリの起動を跨いで利用できるようにします。
そして、SwiftDataとSwiftUIを統合して、データを再フェッチして画面に表示できます。

SwiftDataフレームワークは、既存のモデルクラスを補完するように設計されています。
このSwiftDataは、Swiftコードでアプリのスキーマを表現的に記述できるマクロやプロパティラッパーなどのツールを提供します。
それによって、モデルや移行マッピングファイルなどの外部依存関係への依存を排除します。

### Turn classes into models to make them persistable

SwiftDataでモデルクラスのインスタンスを保存するには、フレームワークをインポートし、`Model()`マクロでそのクラスにアノテーションをマークします。
`Model`マクロはクラスを更新して、`PersistentModel`プロトコルに準拠させます。
`PersistentModel`は「SwiftDataがクラスを解析して、内部スキーマを生成する」ためのプロトコルです。
さらに、マクロは`Observabel`プロトコルの適合性も追加するので、クラスでの変更が追跡可能になります。

```swift:
import SwiftData

// Annotate new or existing model classes with the @Model macro.
@Model
class Trip {
    var name: String
    var destination: String
    var startDate: Date
    var endDate: Date
    var accommodation: Accommodation?
}
```

デフォルトでSwiftDataは計算プロパティを除く、ほとのどのプロパティをサポートします。
これには`Bool`、`Int`、`String`などのプリミティブ型と、構造体、列挙型、および「`Codable`プロトコルに準拠したその他の値型オブジェクト」などの複雑な型が含まれます。

モデルクラスを定義するコードは、アプリのモデルレイヤーにおける「信頼できる情報源」として機能し、それによってSwiftDataは永続データの一貫性を維持します。

### Customize the persistence behavior of model attributes

属性は、SwiftDataが管理するモデルクラスのプロパティです。
ほとんどの場合、属性に対するフレームワークのデフォルトの動作で十分です。
ただし、SwiftDataが特定の属性について永続性をカスタマイズしたい場合は、スキーマのマクロを使用してください。
例えば、属性の値が「そのモデルのすべてのインスタンスで一意である」ことを指定することで、モデルデータの競合を回避したい場合があります。

属性の動作をカスタマイズするには、`Attribute(_:originalName:hashModifier:)`マクロでプロパティにアノテーションをマークし、目的の動作を駆動するオプションの値を指定します。

```swift:ユニーク属性のプロパティ
@Attribute(.unique) var name: String
```

固有の制約を強制する以外に、`@Attribute`は、削除された値の保存、Spotlightのインデックス作成、暗号化などをサポートしています。
基礎となるモデルデータに元の名前を保持したい場合は、`@Attribute`マクロを使用して名前が変更された属性を正しく処理することもできます。

モデルに、型がモデル（またはモデルのコレクション）でもある属性が含まれている場合、SwiftDataはそれらのモデル間の関係を暗黙的に管理します。
デフォルトでは、フレームワークは関連するモデルインスタンスを削除した後、関係属性を`nil`に設定します。
別の削除ルールを指定するには、`Relationship(_:deleteRule:minimumModelCount:maximumModelCount:originalName:inverse:hashModifier:)`マクロでプロパティにアノテーションをマークします。
例えば、旅行を削除するときに、関連する宿泊施設を削除したい場合があります。

```swift:リレーションシップの削除ルールをカスタムする
@Relationship(.cascade) var accommodation: Accommodation?
```

SwiftDataは、デフォルトでモデルのすべての非計算属性を保持しますが、必ずしもこれが起こるとは限りません。
例えば、クラスのプロパティには「今後の旅行の目的地の現在の天気」など、保存する必要のない一時的なデータしか含まれていないこともあります。
このようなシナリオでは、`Transient()`マクロでこれらのプロパティにアノテーションをマークすると、SwiftDataはその値をディスクに書き込みません。

```swift:
@Transient var destinationWeather = Weather.current()
```

### Configure the model storage

SwiftDataがモデルを調べて必要なスキーマを生成する前に、実行時にどのモデルを永続させるか、およびオプションで基礎となるストレージに使用する構成を伝える必要があります。
例えば、テストを実行するときにストレージをメモリにのみ存在させたり、デバイス間でモデルデータを同期するときに特定のCloudKitコンテナを使用したりできます。

デフォルトのストレージを設定するには、ビュー（またはシーン）の`modelContainer(for:inMemory:isAutosaveEnabled:isUndoEnabled:onSetup:)`モディファイアに「永続化するモデル型の配列」を指定します。
このモディファイアをビューで使用する場合は、すべてのビュー階層が適切に環境を継承するように「ビュー階層のトップ」で適用してください。

```swift:
import SwiftUI
import SwiftData

@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: [
                    Trip.self,
                    Accommodation.self
                ])
        }
    }
}
```

SwiftUIを使用していない場合は、適切なイニシャライザを使用して手動でモデルコンテナを作成します。

```swift: SwiftUIではない場合のモデルコンテナ作成
import SwiftData

let container = try ModelContainer([
    Trip.self, 
    Accommodation.self
])
```

> Tip
モデル型にリレーションが含まれている場合は、配列からデスティネーションのモデル型を省略できます。
SwiftDataは自動的にモデルのリレーションを横断し、任意のデスティネーションのモデル型を含めます。

`ModelConfiguration`を使用して、独自のストレージを作成することも可能です。
この型は以下の設定事項を含む、いくつかのオプションを提供します。

- ストレージをメモリ内だけの存在にする
- ストレージを読み取り専用にする
- アプリは特定のアプリグループを使用して、モデルデータを保存する

```swift:
let configuration = ModelConfiguration(isStoredInMemoryOnly: true, allowsSave: false)

let container = try ModelContainer(
    for: Trip.self, Accommodation.self, 
    configurations: configuration
)
```

> Important
iCloudの自動同期はCloudKitエンタイトルメントの存在に依存し、SwiftDataはそのエンタイトルメントで見つかった最初のコンテナを使用します。
アプリに特定のコンテナが必要な場合は、そのコンテナを`ModelConfiguration`型インスタンスで指定します。

### Save models for later use

実行時にモデルクラスのインスタンスを管理するには、**モデルコンテキスト** を使用します。
これは、「メモリ上にあるデータとモデルコンテナ間の調整役」を担うオブジェクトであり、データを永続化するものです。
「メインアクターにバインドされているモデルコンテナ」のコンテキストを取得するには、`modelContext`環境変数を使用します。

```swift:
import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var context  // 環境変数のコンテキストを取得
}
```

ビューの外、またはSwiftUIを使用していない場合は、モデルコンテナを使用して同じアクターバウンドコンテキストに直接アクセスします。


```swift:
let context = container.mainContext
```

どちらの場合も、返されたコンテキストは「保存していない変更が残っているか」を定期的にチェックします。
未保存の変更がある場合は、それらの暗黙的な保存を代行します。
手動で作成するコンテキストでは、`autosaveEnabled`プロパティを`true`にして、同じ動作を取得します。

SwiftDataがモデルインスタンスを永続化し、その変更の追跡を開始するには、インスタンスをコンテキストに挿入します。

```swift:
var trip = Trip(name: name, 
                destination: destination, 
                startDate: startDate, 
                endDate: endDate)

context.insert(trip)
```

挿入に続いて、コンテキストの`save()`メソッドを呼び出してすぐに保存するか、代わりにコンテキストで暗黙的に保存できます。
コンテキストは「既知のモデルインスタンスへの変更」を自動的に追跡し、それらの変更を後続のセーブに含めます。
保存に加えて、コンテキストを使用してモデルインスタンスをフェッチ、列挙、削除できます。

### Fetch models for display or additional processing

モデルデータの永続化した後は、そのデータを取得し、モデルインスタンスとして具体化し、それらのインスタンスをビューに表示したり、その他のアクションを実行したりします。
SwiftDataは、フェッチを実行するための`Query`プロパティラッパーと`FetchDescriptor`型を提供します。

モデルインスタンスを取得したり、検索基準や優先ソート順を任意で適用するには、SwiftUIビューで`@Query`を使用します。
`@Model`マクロはモデルクラスに`Observable`プロトコルの適合性を追加するので、SwiftUIは取得したインスタンスに変更が発生するたびにビューを更新できます。

```swift:
import SwiftUI
import SwiftData

struct ContentView: View {
    @Query(sort: \.startDate, order: .reverse) var allTrips: [Trip]
    
    var body: some View {
        List {
            ForEach(allTrips) {
                TripView(for: $0)
            }
        }
    }
}
```

ビューの外側か、SwiftUIを使用していない場合は、`ModelContext`のいずれかのフェッチメソッドを使用してください。
各メソッドは、述語とソート順序を含む`FetchDescriptor`型インスタンスを期待します。
フェッチ記述子はバッチ処理、オフセット、プリフェッチなどに影響を与える設定を追加できます。

```swift:
let context = container.mainContext

let upcomingTrips = FetchDescriptor<Trip>(
    predicate: #Predicate { $0.startDate > Date.now },
    sortBy: [
        .init(\.startDate)
    ]
)
upcomingTrips.fetchLimit = 50
upcomingTrips.includePendingChanges = true


let results = context.fetch(upcomingTrips)
```

## 参考

Apple Developer Documentation
- [Schema.Relationship.DeleteRule](https://developer.apple.com/documentation/swiftdata/schema/relationship/deleterule-swift.enum)
- [ModelContext](https://developer.apple.com/documentation/swiftdata/modelcontext)
- [`Predicate`](https://developer.apple.com/documentation/foundation/predicate)