SwiftUIで＠FirestoreQueryを上手に使おう - collectionPathを変更する方法 -

# はじめに

＠FirestoreQueryとは、以下のようなやつです。

```swift:FirebaseQuery
@FirestoreQuery(collectionPath: "users") var users: [User]
```

こうやって書くだけで自動でFirestoreからusersコレクションのデータを撮ってきてくれる！しかも勝手にUser型に変換してくれる！！嬉しい！！

と、とても便利なはずの＠FirestoreQuery君なのですが、あまり使われてる場面や記事を見かけません。なぜ使いにくいのか、そして使いやすくなる方法を考えたので、ぜひ読んでみてください。

# ＠FirestoreQueryでできること

前述の通り、**書くだけでコレクションのデータ取得・変化の監視**をすることができます。

このとき、`User`は`Codable`に準拠した`struct`である必要があります。

```swift:User
import FirebaseFirestore

struct User: Identifiable, Codable {
    @DocumentID var id: String?
    var name: String
    var email: String
}
```

ここで、`@DocumentID`をつけると勝手にドキュメントのIDを取得してくれます！（RealtimeDatabaseの頃は手動だったから大変だった…良い時代になりました。）

余談：そもそもFirestoreとCodableの相性がすこぶる良いです。

# ＠FirestoreQueryの残念なこと

### Pathに変数を含められない

これが一番残念な点。例えば`NavigationLink`で画面遷移するとします。

```mermaid
flowchart LR
    ContentView[List] --> User1[User1]
    ContentView --> User2[User2]
```

ここで`User`の各々のドキュメントの中に`scores`コレクションがあったとします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2823873/e634ffb9-ade2-ed38-45a0-1e9c4cd1f255.png)

この`scores`コレクションへのパス`users/{document_id}/scores`を**うまく定義できない。つまり、以下の書き方がエラーになります。

```swift:エラーになる
@FirestoreQuery(collectionPath: "users/\(userID)") var users: [User]
// Cannot use instance member 'path' within property initializer; property initializers run before 'self' is available
```

### ドキュメント内のコレクションはうまくとってこれない
これは普通にFirestoreの悪いところですが、上のようにscoresを配置しても、**[Score]として自動でDecodeしてくれません**。
無理やり辞書型でドキュメント内に記述してもいいですが、structとの対応などを考えるとあまり良くないですよね。

また、usersを監視しているだけではscoresを取得できないなどもあり、大変です。

# ＠FirestoreQueryをうまく使おう

さて、本題として、重要なのは**どうやってcollectionPathに変数を含めるか**です。これがどうやっても記事が出てこなかった…

結論から言うと以下のように変更することができます。

```swift:pathの変更
.onAppear {
    $users.path = path
}
```

`users`を`Binding`として扱ってあげる必要があり、しかも`collectionPath`ではなく単に`path`と指定するようです。

### 例

上で作った、`users`の中に`scores`があるものを例にすれば、

```swift:ContentView
import SwiftUI
import FirebaseFirestore

struct User: Identifiable, Codable {
    @DocumentID var id: String?
    var name: String
    var email: String    
}

struct ContentView: View {
    @FirestoreQuery(collectionPath: "users") var users: [User]
    
    var body: some View {
        NavigationStack {
            List(users) { user in
                NavigationLink {
                    NextView(path: $users.path + "/" + user.id! + "/scores")
                } label: {
                    VStack(alignment: .leading) {
                        Text(user.name)
                            .font(.headline)
                        Text(user.email)
                            .font(.subheadline)
                    }
                }
            }
        }
    }
}
```

```swift:NextView
import SwiftUI
import FirebaseFirestore

struct Score: Identifiable, Codable {
    @DocumentID var id: String?
    var score: Int
}

struct NextView: View {
    @FirestoreQuery(collectionPath: "empty") var scores: [Score]
    var path: String
    
    var body: some View {
        List(scores) { score in
            Text(String(score.score))
        }
        .onAppear {
            $scores.path = path
        }
    }
}
```

`NextView`で、最初は`collectionPath`を`"empty"`とするのなどはアンチパターンかも知れませんが、とにかくこうすればうまくいく。というのが言いたかったことです。

# まとめ
手軽にFirestoreの変更を監視できる上、`getDocuments`などを全く書かなくてもなんとかなるのは本当に手軽なので、なにかしら活用できればいいなと思ってこの記事を書きました。

ともあれ、`.onAppear`で変更するしかなかったり、アーキテクチャのことを考えると、Readは`@FirestoreQuery`でView内で行い、Create/Update/DeleteはFirestoreのレポジトリから行う、など分離するのが良くなかったりといった意見もあるかも知れません。この辺はベストプラクティスを探っていきたいなと思っています。