[Swift] macOSでFirebaseを使う ~ おまけ

#単なるおまけです

##DatabaseValue
こんなのあったらユニットテストに便利なんじゃないかなぁとかおもう。
処理が重すぎるので常用するには難ありか。

```swift:DatabaseValue.swift
import Foundation

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 {}


public func isDatabaseValue(_ object: Any) -> Bool {
    
    switch object {
        
    case let array as [Any]: return isDatabaseValue(array)
        
    case let dict as [AnyHashable: Any]: return isDatabaseValue(dict)
        
    case _ as DatabaseValue: return true
        
    default: return false
    }
}

private func isDatabaseValue(_ array: [Any]) -> Bool {
    
    return array.first { !isDatabaseValue($0) } == nil
}

private func isDatabaseValue(_ dict: [AnyHashable: Any]) -> Bool {
    
    guard let dict = dict as? [String: Any] else { return false }
    
    return dict.values.first { !isDatabaseValue($0) } == nil
}
```
##DatabaseModel
意味ない気しかしない

```swift:DatabaseModel.swift
import Foundation
import FirebaseDatabase

protocol DatabaseModel {
    
    var databaseKey: String { get }
}

extension DatabaseReference {
    
    func child(_ model: DatabaseModel) -> DatabaseReference {
        
        return child(model.databaseKey)
    }
}
```
##Result

ちょっと後で使うので最小限の実装。

```swift:Result.swift
enum Result<Value> {
    
    case value(Value)
    
    case error(Error)
}

extension Result {
    
    init(_ f: () throws -> Value) {
        
        do {
            
            self = .value(try f())
            
        } catch {
            
            self = .error(error)
        }
    }
}


extension Result {
    
    @discardableResult
    func ifValue(_ f: (Value) -> Void) -> Result {
        
        if case let .value(value) = self {
            
            f(value)
        }
        
        return self
    }
    
    @discardableResult
    func ifError(_ f: (Error) -> Void) -> Result {
        
        if case let .error(error) = self {
            
            f(error)
        }
        
        return self
    }
}
```

##DatabaseCodable
多分便利。
`DataSnapshot.value`がAnyなため、処理をするための型変換が煩わしく、処理が見えにくくなる問題を解決する。
あくまでも処理を見えやすくするためのもの。
型変換自体のコードは当然必要。

```swift:DatabaseCodable.swift
import Foundation
import FirebaseDatabase

protocol DatabaseDecodable {
    
    init(from: DataSnapshot) throws
}

protocol DatabaseEncodable {
    
    func encode() throws -> Any
}

typealias DatabaseCodable = DatabaseEncodable & DatabaseDecodable

extension DatabaseQuery {
    
    func observe<D: DatabaseDecodable>(_ type: DataEventType, type decodable: D.Type, with handler: @escaping (Result<D>) -> Void) -> DatabaseHandle {
        
        return observe(type) { snapshot in
            
            handler(Result({ try decodable.init(from: snapshot) }))
        }
    }
    
    func observeArray<D: DatabaseDecodable>(_ type: DataEventType, type decodable: D.Type, with handler: @escaping (Result<[D]>) -> Void) -> DatabaseHandle {
        
        return observe(type) { snapshot in
            
            let value = Result {
                
                try snapshot
                    .children
                    .compactMap { $0 as? DataSnapshot }
                    .compactMap { try decodable.init(from: $0) }
            }
            
            handler(value)
        }
    }
}

extension DatabaseReference {
    
    func setEncodableValue<E: DatabaseEncodable>(_ value: E, with handler: @escaping ((Error?, DatabaseReference) -> Void) = { _,_ in }) {
        
        do {
            
            setValue(try value.encode(), withCompletionBlock: handler)
            
        } catch {
            
            handler(error, self)
        }
    }
}
```


##使用例
上の`DatabaseCodable`を使用することで`observe(_:with:)`に渡す関数が、値を変換する処理に目がいって、実際に何をしているのかがわかりづらいという問題を解消できる。

```swift:Memo.swift
enum MemoCodingError: Error {
    
    case notMemo
    
    case hasNotMemo
    
    case hasNotDate
}

extension Memo: DatabaseCodable {
    
    func encode() throws -> Any {
        
        return ["memo": memo, "date": date]
    }
    
    init(from snapshot: DataSnapshot) throws {
        
        guard let dict = snapshot.value as? [String: Any] else { throw MemoCodingError.notMemo }
        guard let memo = dict["memo"] as? String else { throw MemoCodingError.hasNotMemo }
        guard let date = dict["date"] as? TimeInterval else { throw MemoCodingError.hasNotDate }
        
        self.dataKey = snapshot.key
        self.memo = memo
        self.date = date
    }
}
```

```swift:MemoViewController.swift
extension MemoViewController {
    
    private func registerObserver() {
        
        guard let ref = userRef else { return }
        
        let h0 = ref.observe(.childAdded, type: Memo.self) { result in
            
            result
                .ifValue { memo in
                    
                    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)
                }
                .ifError { error in print(error) }
        }
        handles.append(h0)
        
        let h1 = ref.observe(.childChanged, type: Memo.self) { result in
            
            result
                .ifValue { memo in
                    
                    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.")
                    }
                }
                .ifError { error in print(error) }
        }
        handles.append(h1)
    }
}
```

[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を使う ~ Realtime Database編](https://qiita.com/masakihori/items/dbeba6f3c92208b8db9d)