[WWDC 2023] SwiftDataを試してみる

WWDC23で発表されたSwiftDataについて、実際にプログラムを書いて試してみました。

https://developer.apple.com/videos/play/wwdc2023/10154/

## SwiftDataとは
[SwiftData](https://developer.apple.com/xcode/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**みたいなものでしょうか。

## データ定義
```Pokemon.swift
@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の代わりに用いられるもの、と言えるかもしれません。

https://developer.apple.com/documentation/observation/observable-swift.protocol

また、`@Attribute(.unique)`でユニーク制約を表現できたりします。

## SwiftUIで使ってみる

定義したデータはView側でこのように使えたりします。

```PokeListView.swift
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が更新されるようにもなっています。


さて、永続化する処理はどのようにして呼び出すのでしょう。
このプログラムでは、ポケモンの名前やタイプなどを入力すると保存ができるようにしたいです。

```PokeAdditionView.swift
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.

https://developer.apple.com/documentation/swiftdata/modelcontext

いわばデータソースとして扱うためのオブジェクトです。
データの追加には、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.

https://developer.apple.com/documentation/swiftdata/modelcontainer

「アプリのスキーマとモデルストレージの設定を管理するオブジェクト」とのこと。アプリ全体のストレージスタックを生成するようです。
おそらくこのオブジェクトの設定がないとデータを使用したいViewなどでデータ操作ができません。

そのため説明動画に沿って、ModelContainerをセットアップしてみます。

```practice-swiftdataApp.swift
import SwiftUI

@main
struct practice_swiftdataApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Pokemon.self)
    }
}
```

`modelContainer(for: Pokemon.self)` ← このモディファイアが必要です。

メソッド定義を見る感じ、for引数に複数のモデル型を配列でセットできるイニシャライザもある模様。

## 動かしてみる
ポケモンの名前を登録して、リストに出すだけのプログラムを作成しました。
SwiftDataが正しく使えていれば、アプリを一度閉じても登録したポケモンの表示が出るはず。

何も登録してない状態から追加して、アプリを閉じる。そして再度開く。

<img width="200" alt="ezgif.com-video-to-gif.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3472463/5ec594d4-3769-835c-3327-e6c0d4bd8d95.gif">

出ました。大したことをしていないので全てが参考になるわけでは無いですが、Realmなどよりも楽に使えそうなポテンシャルを感じます。

***
(TCAと使うときどうするんだろう。。)