[Swift] macOSでFirebaseを使う ~ Realtime Database編

#Realtime Databaseへのアクセス

##準備

Realtime databaseへのアクセスは`DatabaseReference`を通じて行います。

`DatabaseReference`はデータベースのキーに当たる部分以下を参照します。

```:再掲
root ─ Users ┬ $uid ┬ $id ┬ memo (String)
             │      │　　　└ date (TimeInterval)
             │      │
             │      ├ $id ...
             │
             ├ $uid ...
```

このスキーマですと、`Users`、各`$uid`、各`$id`などに当たります。
今回作成中のメモアプリで使用するのは現在認証されているユーザー用のストレージ部分`$uid`の部分です。

これを`MemoViewController`の`userRef`プロパティとして保持します。
再掲

```swift:MemoViewController.swift
import Cocoa
import FirebaseAuth
import FirebaseDatabase

class MemoViewController: NSViewController {
    
    private var userRef: DatabaseReference?
    private var handles: [DatabaseHandle] = []
    
    // 以下略
}
```

`DatabaseHandle`はKVOの`NSKeyValueObservation`と同じ役割で、監視を終了するときに用います。

ログインログアウトが自由に行えるようになっていますので、その度に'userRef'は書き換える必要があります。
`MemoViewController`はログインすると表示され、ログアウトすると非表示になるので、`viewWillAppear()`、`viewWillDisappear`で書き換えを行うようにします。

```swift:MemoViewController.swift
extension MemoViewController {
    
    override func viewWillAppear() {
        
        guard let uid = Auth.auth().currentUser?.uid else { return }
        
        userRef = Database.database().reference().child("Users").child(uid)
        
        registerObserver()
    }
    
    override func viewWillDisappear() {
        
        handles.forEach { userRef?.removeObserver(withHandle: $0) }
        handles = []
        
        userRef = nil
        
        // 表示内容を空に
        memos = []
        memoTable.reloadData()
    }
}
```

##Realtime Databaseへの書き込み

データの書き込みと削除を行います。
前回作ったPostボタンのアクションおよび `delete:`アクションとして実装します。

```swift:MemoViewController.swift
extension MemoViewController {
    
    @IBAction private func post(_: Any) {
        
        guard canPost else { fatalError("ここに来るとはなさけない。") }
        
        let value: [String: Any] = [
            "date": Date().timeIntervalSince1970,
            "memo": memo
        ]
        
        userRef?.childByAutoId().setValue(value)
        
        self.memo = ""
    }
    
    @IBAction private func delete(_: Any) {
        
        let selectedIndex = memoTable.selectedRow
        guard case 0..<memos.count = selectedIndex else { return }
        
        userRef?.child(memos[selectedIndex].dataKey).removeValue()
    }
}
```
###書き込み
まずはRealtime Databaseへの書き込みです。

```swift
userRef?.childByAutoId().setValue(value)
```
先に作成した `/Users/$uid/`の`DatabaseReference`の`childByAutoId()`メソッドを用いてユニークなidを持った新しい`DatabaseReference`を作成します。
この`DatabaseReference`に`setValue(_:)`で値を設定します。

`setValue(_:)`の引数の型は`Any`のため何でも渡すことが出来ますが、書き込み不可の型の値を渡した場合は失敗します。
`setValue(_:with:)`を用いることでエラーハンドリングが可能です。
`Date`型など前回あげなかった型の中にも書き込み可能な型がありますが、取得が出来ないので、対称性を考えると使用しないほうがよいと思われます。

###削除
次にデータの削除です。

```swift
userRef?.child(memos[selectedIndex].dataKey).removeValue()
```
データの取得のところで書きますが、取得したデータから`Memo`型の値を作成するときに、書き込み時に自動生成したユニークなidを`dataKey`プロパティに設定しています。
このプロパティを用いて、参照先を特定して`removeValue()`メソッドにて削除します。


`post:`アクションが出来たのでPostボタンに接続します。

##Realtime databaseの監視

Realtime databaseでは値の取得は自由には行えません。
KVOのような仕組みを用いて、追加変更などの時にそのデータのコピー(snapshot)が取得できるようになっています。

`viewWillAppear()`メソッドにあった`registerObserver()`メソッドを実装します。

```swift:MemoViewController.swift
extension MemoViewController {
    
    private func registerObserver() {
        
        guard let ref = userRef else { return }
        
        // 追加を監視
        let h0 = ref.observe(.childAdded) { snapshot in
            
            guard let dict = snapshot.value as? [String: Any] else { return }
            guard let memoString = dict["memo"] as? String else { return }
            guard let date = dict["date"] as? TimeInterval else { return }
            
            let memo = Memo(dataKey: snapshot.key, memo: memoString, date: date)
            
            let index = self.memos.index { aMemo in aMemo.date < memo.date } ?? self.memos.count
            let insertIndex = max(index, 0)
            
            self.memos.insert(memo, at: insertIndex)
            self.memoTable.insertRows(at: [insertIndex], withAnimation: .slideDown)
        }
        handles.append(h0)
        
        // 削除を監視
        let h1 = ref.observe(.childRemoved) { snapshot in
            
            guard let dict = snapshot.value as? [String: Any] else { return }
            guard let memoString = dict["memo"] as? String else { return }
            guard let date = dict["date"] as? TimeInterval else { return }
            
            let memo = Memo(dataKey: snapshot.key, memo: memoString, date: date)
            
            if let index = self.memos.index(of: memo) {
                
                self.memos.remove(at: index)
                self.memoTable.removeRows(at: [index], withAnimation: .slideUp)
                
            } else {
                
                print("deletion: memo is not found.")
            }
        }
        handles.append(h1)
    }
}
```

Realtime databaseの監視は`DatabaseReference`のスーパークラスである`DatabaseQuery`クラスの`observe(_:with:)`メソッドによって行います。
仕組みについてはKVOを知っている我々にとっては説明不要ですね。

第一引数はどのイベントを監視するかを指定する、enum `DataEventType`の値です。

 - childAdded
 - childRemoved
 - childChanged
 - childMoved
 - value

の5種類があります。最初の4つについては文字通りのものです。valueは、何でもかんでも監視します。

第二引数は対象のイベントが発生したときに呼び出される関数です。型は`(DataSnapshot) -> Void`。
`DataEventType`がvalue以外の場合は変更などがあったデータの、valueの時は監視対象自身の`DataSnapshot`が渡されます。

`DataSnapshot`はそれが生成された時点でのデータのコピーです。
`value`プロパティに値が、`key`プロパティにキーが入っています。
`value`の型が`Any`なので面倒くさいです。

##完成

完成したと思う。

##DatabaseQuery

`DatabaseQuery`をさらっと流しましたが、これがFirebaseを**すごいKVO**たらしめてるクラスです
監視をするときに「最初の10件だけ」とか「ここからここまで」とか「ソートして」など、柔軟に監視対象を設定できます。

詳細はまだ調べてません！！

##次回

次回は`observe(_:with:)`~~が面倒くさい~~、可読性をあげる、です。

[macOSでFirebaseを使う ~ Realtime Database用準備編](https://qiita.com/masakihori/items/992eb83171d6269a30ba)

[macOSでFirebaseを使う ~ Authentication編](https://qiita.com/masakihori/items/0b49bf902f9aa7f9dccc)

[macOSでFirebaseを使う ~ Realtime Database用準備編](https://qiita.com/masakihori/items/992eb83171d6269a30ba)


[macOSでFirebaseを使う ~ おまけ](https://qiita.com/masakihori/items/9975b938a12d759d5d22)