Cloud Firestore + SwiftUI + Ballcapで始めるFirebaseアプリ開発

Firebase, SwiftUIとも注目度の高いテクノロジーを使うと本当に簡単にアプリが作れてしまいます。
今回は初心者向けに、Cloud FirestoreとSwiftUIを使ってアプリを作る方法を解説します。


## アプリの仕様

今回は、2つのViewを準備してデータの作成、表示、更新、削除ができる簡単なアプリを解説作って行こうと思います。
いわゆるTODOアプリっぽいやつですね。

また、今回はBallcapを使ってアプリを作っていきます。
Ballcapは`cocoapods`を使ってインストールできます。

https://github.com/1amageek/Ballcap-iOS

## モデル定義

まずはデータベースモデル定義をしていきましょう。

```swift
final class Task: Object, DataRepresentable, DataListenable, ObservableObject, Identifiable {

    typealias ID = String

    override class var name: String { "tasks" }

    struct Model: Codable, Modelable {

        var title: String = ""

        var due: ServerTimestamp?
    }

    @Published var data: Task.Model?
    
    var listener: ListenerRegistration?
}
```

色々とごちゃごちゃしてるので順を追ってみていきましょう。

### Ballcap Object継承

Ballcapの`Object`を継承すると__Cloud Firestore__の`Collection`の名前を定義して`DocumentReference`と`StorageReference`を保持することができます。
次の例では`/tasks`を定義しています。

```swift
final class Task: Object {
    override class var name: String { "tasks" }
}
```
__Collectionの名前__

```
/tasks/
```

__DocumentReference__, __StorageReference__

```
/tasks/:id
```

### DataRepresentableプロトコル
`DataRepresentable`プロトコルは`Object`に`Data`を保持させることを担います。
`DataRepresentable`に準拠させることで、`Model`の定義と`data`の保持を要求されます。

```swift
final class Task: Object, DataRepresentable {

    override class var name: String { "tasks" }

    struct Model: Modelable, Codable {
        // Modelを定義
    }
    // Cloud FirestoreのField dataを保持する
    var data: Task.Model?
}
```

開発者は、`Modelable`と`Codable`に準拠した`Model`定義する必要があります。
今回は以下の2のプロパティを定義しました。

```swift
    struct Model: Modelable, Codable {
        // Taskのタイトル
        var title: String = ""
        // Taskの期限
        var due: ServerTimestamp?
    }
```

また、`DataRepresentable`に準拠した`Object`は次のことを行えるようになります。

- `Object`を任意のIDで初期化する
- データの保存、取得、更新、削除

ここまでの機能で`Cloud Firestore`を使って簡単なアプリは作れそうですね。

### DataListenableプロトコル
`DataListenable`プロトコルは`Object`の`Data`の変更を監視するための機能を持っています。`DataListenable`に準拠させると`listener`の保持を要求されます。

```swift
final class Task: Object, DataRepresentable, DataListenable {

    override class var name: String { "tasks" }

    struct Model: Modelable, Codable {
        // Taskのタイトル
        var title: String = ""
        // Taskの期限
        var due: ServerTimestamp?
    }
    // Cloud FirestoreのField dataを保持する
    var data: Task.Model?
    // Cloud FirestoreのField dataの変更を監視する
    var listener: ListenerRegistration?
}
```

ここまでで、Ballcapでの機能追加は終わりです。
次に、SwiftUIに必要なプロトコルへ準拠させていきます。

### ObservableObjectプロトコル

ObservableObjectプロトコルは`@Published`で定義されたプロパティの変更をSwiftUIへ伝える役割を果たします。
今回はもちろん`data`の変更をSwiftUIへ伝えたいので`@Published`を`data`へ追記します。

```swift
final class Task: Object, DataRepresentable, DataListenable, ObservableObject  {

    override class var name: String { "tasks" }

    struct Model: Modelable, Codable {
        // Taskのタイトル
        var title: String = ""
        // Taskの期限
        var due: ServerTimestamp?
    }
    // Cloud FirestoreのField dataを保持する
    @Published var data: Task.Model?
    // Cloud FirestoreのField dataの変更を監視する
    var listener: ListenerRegistration?
}
```

### Identifiableプロトコル

IdentifiableプロトコルはSwiftUIのListなどに必要なプロトコルです。
`id`を指定する必要がありますが、`Object`は`id`を保持しているので`typealias`だけを追記します。

```swift
typealias ID = String
```

```swift
final class Task: Object, DataRepresentable, DataListenable, ObservableObject, Identifiable {
    typealias ID = String

    override class var name: String { "tasks" }

    struct Model: Modelable, Codable {
        // Taskのタイトル
        var title: String = ""
        // Taskの期限
        var due: ServerTimestamp?
    }
    // Cloud FirestoreのField dataを保持する
    @Published var data: Task.Model?
    // Cloud FirestoreのField dataの変更を監視する
    var listener: ListenerRegistration?
}
```

長かったですが必要な定義が整いました。

## Viewを作る

### TaskListViewを作る
まずは`Task`をリスト表示するViewを作っていきましょう。もっともシンプルな形はこれです。
これに機能を追加していきます。

```swift
struct TaskListView: View {

    @State var tasks: [Task] = []

    var body: some View {
        NavigationView {
            List {
                ForEach(tasks) { task in
                    VStack {
                        Text(task[\.title])
                    }
                }
            }
        }
    }
}
```

### CloudFirestoreと連携させてList表示させる

```swift
struct TaskListView: View {

    @State var tasks: [Task] = []

    let dataSource: DataSource<Task> = Task.order(by: "updatedAt").limit(30).dataSource()

    var body: some View {
        NavigationView {
            List {
                ForEach(tasks) { task in
                    VStack {
                        Text(task[\.title])
                    }
                }
            }
            .onAppear(perform: {
                self.dataSource
                    .retrieve(from: { (_, snapshot, done) in
                        let task: Task = Task(snapshot: snapshot)!
                        done(task)
                    })
                    .onChanged({ (_, snapshot) in
                        self.tasks = snapshot.after
                    })
                    .listen()
            })
        }
    }
}
```

`DataSource`が登場しました。ここで`DataSource`について見ていきましょう。

#### DataSource

`DataSource`はBallcapがサポートするCloud Firestoreの`Collection`を管理する機能です。次のように`DataSource`を定義することができます。

`DataSource`は他にも`arrayContains`などの`CollectionReference`がもつ全ての機能をサポートしています。

```swift
let dataSource: DataSource<Task> = Task
    .order(by: "updatedAt")
    .limit(30)
    .dataSource()
```

上の例では`Task`の`Collection`を`updatedAt`の順番に`30`件取得する`DataSource`を表します。

#### onAppear
次に`onAppear`について見ていきましょう。`onAppear`は`View`が表示されたタイミングで一度だけ呼ばれます。ここで`DataSource`の監視をスタートし、変更をキャッチできるようにしています。

```swift
            .onAppear(perform: {
                self.dataSource
                    .retrieve(from: { (_, snapshot, done) in
                        let task: Task = Task(snapshot: snapshot)!
                        done(task)
                    })
                    .onChanged({ (_, dataSourceSnapshot) in
                        self.tasks = dataSourceSnapshot.after
                    })
                    .listen()
            })
```

#### retrieve

`retrieve`では、`CollectionReference`から取得した`Snapshot`を`Task`に変換しています。

```swift
.retrieve(from: { (_, snapshot, done) in
    let task: Task = Task(snapshot: snapshot)!
    done(task)
})
```

#### onChanged

`onChanged`では、取得したデータの配列を管理しています。
クロージャの中で渡される`dataSourceSnapshot`に変更情報が含まれます。

```swift
.onChanged({ (_, dataSourceSnapshot) in
    self.tasks = dataSourceSnapshot.after
})
```

`DataSource`の中で`Snapshot`が定義されており、この`Snapshot`には、変更前のデータ`before`変更後のデータ`after`変更されたデータ`changes`が格納されていています。
今回は変更後のデータだけを`tasks`へ渡しています。

```swift
public final class DataSource<T: DataRepresentable>: ExpressibleByArrayLiteral {

    public typealias Changes = (deletions: [Element], insertions: [Element], modifications: [Element])

    public struct Snapshot {

        public let before: [Element]

        public let after: [Element]

        public let changes: Changes
    }
}
```

#### listen

`listen`は`DataSource`が監視を開始するためのメソッドです。このメソッドをコールしないと`DataSource`は起動しないので忘れないようにコールしましょう。

`DataSource`は多くの機能を提供しているので詳細はこちらをご覧ください。
https://github.com/1amageek/Ballcap-iOS#datasource

### 新しいTaskを作る

新しい`Task`が作れるように`NavigationBar`に機能を追加して`NewTaskView`へ遷移できるようにしました。

```swift
struct TaskListView: View {

    @State var isPresented: Bool = false

    @State var tasks: [Task] = []

    let dataSource: DataSource<Task> = Task.order(by: "updatedAt").dataSource()

    var body: some View {
        NavigationView {
            List {
                ForEach(tasks) { task in
                    VStack {
                        Text(task[\.title])
                    }
                }
            }
            .onAppear {
                self.dataSource
                    .retrieve(from: { (_, snapshot, done) in
                        let task: Task = Task(snapshot: snapshot)!
                        done(task)
                    })
                    .onChanged({ (_, snapshot) in
                        self.tasks = snapshot.after
                    })
                    .listen()
            }
            .navigationBarTitle("Todo")
            .navigationBarItems(trailing: Button("追加") {
                self.isPresented.toggle()
            })
            .sheet(isPresented: $isPresented, content: {
                NewTaskView()
            })
        }
    }
}
```

#### NewTaskView

新しい`Task`を作るための`NewTaskView`は次のようになっています。

```swift
struct NewTaskView: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @ObservedObject(initialValue: Task()) var task: Task

    var body: some View {
        NavigationView {
            TextField("新しいタスク", text: self.$task[\.title])
            .navigationBarTitle("新しいタスク")
            .navigationBarItems(trailing: Button("保存") {
                self.task.save()
                self.presentationMode.wrappedValue.dismiss()
            })
        }
    }
}
```

まず次の変数に注目しましょう。
`NewTaskView`では新しい`Task`を定義したいので次のように`View`に変数として保持するようになってます。また`ObservedObject`となっていて変更を受け取れるようになっています。

```swift
@ObservedObject(initialValue: Task()) var task: Task
```

次に、`TextField`の`text`に`Task`の`title`をバインドしましょう。

```swift
TextField("新しいタスク", text: self.$task[\.title])
```

保存ボタンを押すと、タスクが保存され閉じるようになっています。

```swift
self.task.save()
self.presentationMode.wrappedValue.dismiss()
```

これで新しいタスクを追加することができました。
実際に実行してみると、新しいタスクが追加されていることが見えると思います。


### Taskの編集もしたいのでNewTaskViewをEditTaskViewへ

新規追加だけでなく編集もできるようにしておきましょう。

```swift
struct EditTaskView: View {

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @ObservedObject var task: Task

    var body: some View {
        NavigationView {
            TextField("新しいタスク", text: self.$task[\.title])
                .padding()
                .navigationBarTitle("新しいタスク")
                .navigationBarItems(trailing: Button("保存") {
                    self.task.save()
                    self.presentationMode.wrappedValue.dismiss()
                })
        }
    }
}
```

変更したのは変数の部分だけで、`initialValue`を削除し、外部から与えてもらう形にしました。

```swift
@ObservedObject var task: Task
```

`TaskListView`も変更します。

```swift
struct TaskListView: View {

    enum Presentation: View, Hashable, Identifiable {
        case new
        case edit(task: Task)
        var body: some View {
            switch self {
            case .new: return  AnyView(EditTaskView(task: Task()))
            case .edit(let task): return AnyView(EditTaskView(task: task))
            }
        }
    }

    @State var presentation: Presentation?

    @State var tasks: [Task] = []

    let dataSource: DataSource<Task> = Task.order(by: "updatedAt").dataSource()

    var body: some View {
        NavigationView {
            List {
                ForEach(tasks) { task in
                    VStack {
                        Text(task[\.title])
                    }
                    .contextMenu {
                        Button("編集") {
                            self.presentation = .edit(task: task.copy())
                        }
                    }
                }
            }
            .onAppear {
                self.dataSource
                    .retrieve(from: { (_, snapshot, done) in
                        let task: Task = Task(snapshot: snapshot)!
                        done(task)
                    })
                    .onChanged({ (_, snapshot) in
                        self.tasks = snapshot.after
                    })
                    .listen()
            }
            .sheet(item: self.$presentation) { $0 }
            .navigationBarTitle("Todo")
            .navigationBarItems(trailing: Button("追加") {
                self.presentation = .new
            })

        }
    }
}
```

大きな変更をみていきます。次のように`Presentation`を準備しました。
これに関しては別の記事でまとめてますのでこちらをご覧ください。

- [SwiftUIのsheetを出し分ける](https://qiita.com/1amageek/items/e90e1cfb0ad497e8b27a)

```swift
    enum Presentation: View, Hashable, Identifiable {
        case new
        case edit(task: Task)
        var body: some View {
            switch self {
            case .new: return  AnyView(EditTaskView(task: Task()))
            case .edit(let task): return AnyView(EditTaskView(task: task))
            }
        }
    }
    @State var presentation: Presentation?
```

__新しいタスクを作る時__

```swift
self.presentation = .new
```

__タスクを編集する時__
タスクを編集する時は、`.edit`へ現在のタスクのコピーを渡すようにしています。
`EditTaskView`にそのまま`ObservedObject`を渡してしまうと、保存せずとも編集された情報を`TaskListView`が受け取ってしまうため、編集を途中でキャンセルしたとしてもデータが変更されてしまいます。それを防ぐため別の`Object`として`EditTaskView`へ渡しています。

```swift
ForEach(tasks) { task in
    VStack {
        Text(task[\.title])
    }
    .contextMenu {
        Button("編集") {
            self.presentation = .edit(task: task.copy())
        }
    }
}
```

### タスクを完了と削除

ここまでで、タスクの新規作成、編集を行うことができるようになりました。
次にタスクの完了と削除を実装しましょう。今回は編集の時にも使った`contextMenu`を使って実装しました。

```swift
List {
    ForEach(tasks) { task in
        VStack {
            Text(task[\.title])
        }
        .contextMenu {
            Button("完了") {
                task[\.isCompleted] = true
                task.update()
            }
            Button("編集") {
                self.presentation = .edit(task: task)
            }
            Button("削除") {
                task.delete()
            }
        }
    }
}
```

#### タスクの完了

タスクの完了をさせるために、`isCompleted`を`true`にして更新しています。

```swift
task[\.isCompleted] = true
task.update()
```

#### タスクの削除

タスクの削除には`delete`を呼び出すだけです。

```swift
task.delete()
```