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

[前回](https://qiita.com/masakihori/items/0b49bf902f9aa7f9dccc)の続きです

## Realtime Database

実装に移る前に、mscOSプログラマがFirebaseの機構を理解するためのたった一つの言葉を書いておきます。

**FirebaseはすごいKVC/KVO**

これであなたはFirebaseをほとんど理解しました。


##作るもの
個人用のメモ

##仕様
0. 認証済みユーザーのみ読み書きできる
0. ほかのユーザーのメモは読み書きできない
0. メモは「作成日時」「内容」で構成される

ごくシンプルに。

##Realtime Databaseの設定
仕様1.2.を実現するためにFirebase側で設定を行います。
[Firebase console](https://console.firebase.google.com/)へ移動します。
「Detabase」から「Realtime Database」の「データベースを作成」を選択します。
![スクリーンショット (7).png](https://qiita-image-store.s3.amazonaws.com/0/2117/54ab2c08-1c69-1046-1bcd-e8544aab8888.png)

セキュリティールールはすぐ変更しますので、どちらを選んでも構いません。
![スクリーンショット (8).png](https://qiita-image-store.s3.amazonaws.com/0/2117/1ad15928-60b3-b770-bb0e-60386c2a8b1f.png)

「ルール」に移動しルールを以下のように設定します。
<img width="748" alt="スクリーンショット_2018-07-15_11_31_57.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/2117/8eef4d22-ab2b-545d-02ce-4dadba347478.jpeg">


```json:ルール
{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null",
    
    "users": {
      "$uid": {
        ".read": "$uid == auth.uid",
        ".write": "$uid == auth.uid",
      }
    }
  }
}
```
やっていることは

0. データベースへのアクセスは認証済みでなければならない
0. `users\$uid`以下へのアクセスはuidが`$uid`のユーザーのみ許可

です。
*rootの read, writeはこれでいいのか自信がないです。*

##Realtime Databaseの概要
Realtime Databaseの構造は、rootを根とする一つの巨大な`Dictionany`です。
Swift的にはこうなっていると考えると分かりやすいです。

```swift
protocol DatabaseValue {}

extension String: DatabaseValue {}
extension Int: DatabaseValue {}
extension Float: DatabaseValue {}
extension Double: DatabaseValue {}
extension Bool: DatabaseValue {}

extension Array: DatabaseValue where Element: DatabaseValue {}
extension Dictionary: DatabaseValue where Key == String, Value: DatabaseValue {}

struct RealtimeDatabase {

    let root: DatabaseValue
}
```
*詳細な仕様が見つけられてないので、もしかすると使用可能な型がまだあるかもしれません。*

各要素へはKeypathでのアクセスのが可能です。

```swift
// Realtime Database のrootのReference
let root = Database.database().reference()

// キーによるアクセス
let config01 = root.child("room1").child("config")

// キーパスによるアクセス。セパレータは / 
let config02 = root.child("room1/config")
```

値の設定はKVCのように行います。

```swift
root.child("room1").child("config").setValue(["limit": 1000])
```

値の取得は普通には出来ず、KVOのObserverのような設定が必要です。

```swift

// "room1/config"の変更を監視
let handler = root.child("room1").child("config").observe(.childChanged) { snapshot in
    
    guard let dict = snapshot.value as? [String: Any] else { return }
    guard let limit = dict["limit") as? Int else { return }
    
    print(limit)
}
```
オブザーバはそのままでは~~値の取得が大変面倒くさい~~可読性が落ちるので、後ほどextensionを作ります。

##スキーマ定義
フリーダムなNoSQLなので必要はないのですが、スキーマ定義はしておかないと~~あとで死にます~~問題があります。


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

`$uid`はユーザー固有のidです。
`$id`はメモにユニークなidです。

各ユーザーが個別のメモのリスト（実際には辞書）を持っているようになっている。
各メモはメモ自身と作成日時を持っています。

##Modelの定義
メモのモデルを作っておきます。

```swift:Memo.swift
struct Memo {
    
    // realtime database の key
    let dataKey: String
    
    let memo: String
    
    let date: TimeInterval
}

extension Memo: Equatable {
    
    static func == (lhs: Memo, rhs: Memo) -> Bool {
        
        return lhs.dataKey == rhs.dataKey
    }
}
```
`dataKey`はスキーマ定義の`$id`に当たるものです。
持っておくと便利なこともあったりなかったり。

##ユーザーインターフェイスの作成

メモの表示はViewベースの`NSTableView`を使います。

###NSTableCellView

まずは`NSTableCellView`のサブクラス`MemoCellView`を作ります。

```swift:MemoCellView.swift
import Cocoa

class MemoCellView: NSTableCellView {

    @IBOutlet private var memoField: NSTextField!
    
    var memo: Memo? {
        
        didSet {
            
            textField?.objectValue = memo.map { Date(timeIntervalSince1970: $0.date) }
            
            memoField.stringValue = memo?.memo ?? ""
        }
    }
    
    static var identifier: NSUserInterfaceItemIdentifier {
        
        return NSUserInterfaceItemIdentifier("memo")
    }
    
    override var backgroundStyle: NSView.BackgroundStyle {
        
        didSet {
            
            switch backgroundStyle {
                
            case .dark:
                memoField.textColor = .white
                
            case .light, .raised, .lowered:
                memoField.textColor = .controlTextColor
            }
        }
    }
}
```
日時の表示には`NSTableCellView`の`textField`を利用します。
`memo`が設定されたらそれを表示するだけです。

###NSViewController
次に`NSViewController`のサブクラス`MemoViewController`を作ります。
xib付きで作成してください。

これもCocoaBindingsを利用します。

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

class MemoViewController: NSViewController {
    
    private var userRef: DatabaseReference?
    private var handles: [DatabaseHandle] = []
    
    private var memos: [Memo] = []
    
    @IBOutlet private var memoTable: NSTableView!

    @objc dynamic private var memo: String = ""
    
    @objc dynamic private var canPost: Bool {
        
        return memo != ""
    }
    
    override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
        
        if key == #keyPath(canPost) {
            
            return [#keyPath(memo)]
        }
        
        return []
    }
}
```

謎のプロパティがありますが、とりあえず無視してください。

`memos`プロパティの内容を`memoTable`に表示するだけです。

###見た目

UIは次のようにしました。

<img width="560" alt="スクリーンショット_2018-07-15_11_03_57-2.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/2117/3a537883-4099-546d-9ff4-4e6628b71122.jpeg">

ちょっとアレですが、そこは目をつむってください。

一番上の`NSTextField`の`value`に`File's Owner`の`memo`をbind、`Continuously Updates Value`をチェック。
Postボタンの`Enabled`に `File's Owner`の`canPost `をbind。アクションは後ほど。
Sign outボタンのアクションを`First Responder`の`signOut:`に接続。
`File's Owner`の`memoTable`アウトレットに`NSTableView`を接続。
`NSTableView`の`delegate`および`dataSource`を`File's Owner`に接続。
`NSCellView`の`Custom Class`を`MemoCellView`に、`identifier`を`memo`に設定。
`NSCellView`内に`NSTextField`を配置して、これを`MemoCellView`の`memoField`アウトレットに接続。
元から付いてる`NSTextField`に`DateFormatter`を設定。

`Autolayout`もしくは`Autoresizing`はいい感じに設定してください。

###NSTableViewDelegate & NSTableViewDataSource
`NSTableViewDelegate`および`NSTableViewDataSource`は以下の通りです。

```swift:MemoViewController.swift
extension MemoViewController: NSTableViewDelegate {
    
    func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
        
        guard let memoView = tableView.makeView(withIdentifier: MemoCellView.identifier, owner: nil) as? MemoCellView else {
            
            fatalError("Can not get MemoCellView.")
        }
        
        memoView.memo = memos[row]
        
        return memoView
    }
}

extension MemoViewController: NSTableViewDataSource {
    
    func numberOfRows(in tableView: NSTableView) -> Int {
        
        return memos.count
    }
}
```
エラー処理をはしょってますが、普通の実装です。特に問題ないでしょう。

###メインウインドウ

作った`MemoViewController`を`MainWindow`に貼り付けます。

```swift:MainWindowController.swift
import Cocoa

class MainWindowController: NSWindowController {

    override var windowNibName: NSNib.Name {
        
        return NSNib.Name("MainWindowController")
    }
    
    override func windowDidLoad() {
        
        super.windowDidLoad()
        
        self.contentViewController = MemoViewController()
    }
}
```

気になるようでしたら前回`MainWindowController.xib`に貼り付けたボタンは外してください。
外さなくても問題ないです。

##次回

Realtime Databaseへのアクセスを行います。


[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/dbeba6f3c92208b8db9d)

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